[Libguestfs] [PATCH v2 for discussion only] New event API (RHBZ#664558).

Richard W.M. Jones
Fri, 11 Mar 2011 21:47:59 -0000

API is still the same as in the previous message.

This just continues with the implementation.

Rich.

--
Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones
virt-top is 'top' for virtual machines.  Tiny program with many
powerful monitoring features, net stats, disk stats, logging, etc.
http://et.redhat.com/~rjones/virt-top
-------------- next part --------------
>From 33f6b4b03628ab73fc17cbc67213f81e9d1b0da9 Mon Sep 17 00:00:00 2001
From: Richard W.M. Jones <rjones at redhat.com>
Date: Thu, 10 Mar 2011 12:32:22 +0000
Subject: [PATCH] New event API (RHBZ#664558).

This API allows more than one callback to be registered for each
event, makes it possible to call the API from other languages, and
allows [nearly all] log, debug and trace messages to be rerouted from
stderr.

An older version of this API was discussed on the mailing list here:
https://www.redhat.com/archives/libguestfs/2010-December/msg00081.html
https://www.redhat.com/archives/libguestfs/2011-January/msg00012.html

This also updates guestfish to use the new API for its progress bars.
---
 .gitignore                     |    2 +
 capitests/Makefile.am          |   13 ++-
 capitests/test-debug-to-file.c |   89 ++++++++++++
 fish/fish.c                    |    3 +-
 fish/fish.h                    |    2 +-
 fish/progress.c                |   13 ++-
 fish/reopen.c                  |    3 +-
 generator/.depend              |    8 +-
 generator/Makefile.am          |    1 +
 generator/generator_actions.ml |   22 ++--
 generator/generator_c.ml       |  189 +++++++++++++++++--------
 generator/generator_events.ml  |   40 ++++++
 po/POTFILES.in                 |    1 +
 src/Makefile.am                |    1 +
 src/appliance.c                |    6 +-
 src/events.c                   |  301 ++++++++++++++++++++++++++++++++++++++++
 src/guestfs-internal.h         |   43 +++++--
 src/guestfs.c                  |  154 +++++++++++++--------
 src/guestfs.pod                |  244 +++++++++++++++++++++++---------
 src/inspect.c                  |   24 ++--
 src/launch.c                   |   38 +++--
 src/proto.c                    |   76 +++++------
 22 files changed, 984 insertions(+), 289 deletions(-)
 create mode 100644 capitests/test-debug-to-file.c
 create mode 100644 generator/generator_events.ml
 create mode 100644 src/events.c

diff --git a/.gitignore b/.gitignore
index 1511c4a..28bd352 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,11 +10,13 @@ appliance/supermin.d
 autom4te.cache
 *.bak
 bindtests.tmp
+capitests/test.log
 capitests/test-add-drive-opts
 capitests/test-add-libvirt-dom
 capitests/test-command
 capitests/test-config
 capitests/test-create-handle
+capitests/test-debug-to-file
 capitests/test-just-header
 capitests/test-last-errno
 capitests/test*.img
diff --git a/capitests/Makefile.am b/capitests/Makefile.am
index 542c4fb..d1acec7 100644
--- a/capitests/Makefile.am
+++ b/capitests/Makefile.am
@@ -30,7 +30,8 @@ check_PROGRAMS = \
 	test-create-handle \
 	test-config \
 	test-add-drive-opts \
-	test-last-errno
+	test-last-errno \
+	test-debug-to-file

 TESTS = \
 	tests \
@@ -38,7 +39,8 @@ TESTS = \
 	test-create-handle \
 	test-config \
 	test-add-drive-opts \
-	test-last-errno
+	test-last-errno \
+	test-debug-to-file

 # The API behind this test is not baked yet.
 #if HAVE_LIBVIRT
@@ -103,6 +105,13 @@ test_last_errno_CFLAGS = \
 test_last_errno_LDADD = \
 	$(top_builddir)/src/libguestfs.la

+test_debug_to_file_SOURCES = test-debug-to-file.c
+test_debug_to_file_CFLAGS = \
+	-I$(top_srcdir)/src -I$(top_builddir)/src \
+	$(WARN_CFLAGS) $(WERROR_CFLAGS)
+test_debug_to_file_LDADD = \
+	$(top_builddir)/src/libguestfs.la
+
 #if HAVE_LIBVIRT
 #test_add_libvirt_dom_SOURCES = test-add-libvirt-dom.c
 #test_add_libvirt_dom_CFLAGS = \
diff --git a/capitests/test-debug-to-file.c b/capitests/test-debug-to-file.c
new file mode 100644
index 0000000..6d1b619
--- /dev/null
+++ b/capitests/test-debug-to-file.c
@@ -0,0 +1,89 @@
+/* libguestfs
+ * Copyright (C) 2011 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* Test that we can use the new event API to capture all debugging
+ * messages to a file.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "guestfs.h"
+
+static void
+debug_to_file (guestfs_h *g,
+               void *opaque,
+               uint64_t event,
+               int event_handle,
+               int flags,
+               const char *buf, size_t buf_len,
+               const uint64_t *array, size_t array_len)
+{
+  FILE *fp = opaque;
+
+  fwrite (buf, 1, buf_len, fp);
+}
+
+int
+main (int argc, char *argv[])
+{
+  guestfs_h *g;
+  const char *filename = "test.log";
+  FILE *debugfp;
+
+  debugfp = fopen (filename, "w");
+  if (debugfp == NULL) {
+    perror (filename);
+    exit (EXIT_FAILURE);
+  }
+
+  g = guestfs_create ();
+  if (g == NULL) {
+    fprintf (stderr, "failed to create handle\n");
+    exit (EXIT_FAILURE);
+  }
+
+  if (guestfs_set_event_callback
+      (g, debug_to_file,
+       GUESTFS_EVENT_LIBRARY|GUESTFS_EVENT_APPLIANCE|GUESTFS_EVENT_TRACE,
+       0, debugfp) == -1)
+    exit (EXIT_FAILURE);
+
+  if (guestfs_set_verbose (g, 1) == -1)
+    exit (EXIT_FAILURE);
+
+  if (guestfs_set_trace (g, 1) == -1)
+    exit (EXIT_FAILURE);
+
+  if (guestfs_add_drive_opts (g, "/dev/null",
+                              GUESTFS_ADD_DRIVE_OPTS_FORMAT, "raw",
+                              GUESTFS_ADD_DRIVE_OPTS_READONLY, 1,
+                              -1) == -1)
+    exit (EXIT_FAILURE);
+
+  if (guestfs_launch (g) == -1)
+    exit (EXIT_FAILURE);
+
+  guestfs_close (g);
+
+  exit (EXIT_SUCCESS);
+}
diff --git a/fish/fish.c b/fish/fish.c
index b62c098..3ed200c 100644
--- a/fish/fish.c
+++ b/fish/fish.c
@@ -499,7 +499,8 @@ main (int argc, char *argv[])
     : (optind >= argc && isatty (0));

   if (progress_bars)
-    guestfs_set_progress_callback (g, progress_callback, NULL);
+    guestfs_set_event_callback (g, progress_callback,
+                                GUESTFS_EVENT_PROGRESS, 0, NULL);

   /* Interactive, shell script, or command(s) on the command line? */
   if (optind >= argc) {
diff --git a/fish/fish.h b/fish/fish.h
index da0c6a7..114e8a8 100644
--- a/fish/fish.h
+++ b/fish/fish.h
@@ -146,7 +146,7 @@ extern int vg_lv_parse (const char *device, char **vg, char
**lv);

 /* in progress.c */
 extern void reset_progress_bar (void);
-extern void progress_callback (guestfs_h *g, void *data, int proc_nr, int
serial, uint64_t position, uint64_t total);
+extern void progress_callback (guestfs_h *g, void *data, uint64_t event, int
event_handle, int flags, const char *buf, size_t buf_len, const uint64_t *array,
size_t array_len);

 /* in rc.c (remote control) */
 extern void rc_listen (void) __attribute__((noreturn));
diff --git a/fish/progress.c b/fish/progress.c
index 27dfbec..6a89ae0 100644
--- a/fish/progress.c
+++ b/fish/progress.c
@@ -167,9 +167,18 @@ estimate_remaining_time (double ratio)
 /* Callback which displays a progress bar. */
 void
 progress_callback (guestfs_h *g, void *data,
-                   int proc_nr, int serial,
-                   uint64_t position, uint64_t total)
+                   uint64_t event, int event_handle, int flags,
+                   const char *buf, size_t buf_len,
+                   const uint64_t *array, size_t array_len)
 {
+  if (array_len < 4)
+    return;
+
+  /*uint64_t proc_nr = array[0];*/
+  /*uint64_t serial = array[1];*/
+  uint64_t position = array[2];
+  uint64_t total = array[3];
+
   if (have_terminfo == 0) {
   dumb:
     printf ("%" PRIu64 "/%" PRIu64 "\n",
position, total);
diff --git a/fish/reopen.c b/fish/reopen.c
index b076982..67a845c 100644
--- a/fish/reopen.c
+++ b/fish/reopen.c
@@ -67,7 +67,8 @@ run_reopen (const char *cmd, size_t argc, char *argv[])
     guestfs_set_path (g2, p);

   if (progress_bars)
-    guestfs_set_progress_callback (g2, progress_callback, NULL);
+    guestfs_set_event_callback (g2, progress_callback,
+                                GUESTFS_EVENT_PROGRESS, 0, NULL);

   /* Close the original handle. */
   guestfs_close (g);
diff --git a/generator/.depend b/generator/.depend
index 4ea8040..afd1c1e 100644
--- a/generator/.depend
+++ b/generator/.depend
@@ -21,6 +21,8 @@ generator_optgroups.cmx: generator_types.cmx
generator_actions.cmx
 generator_prepopts.cmi:
 generator_prepopts.cmo: generator_prepopts.cmi
 generator_prepopts.cmx: generator_prepopts.cmi
+generator_events.cmo: generator_utils.cmi
+generator_events.cmx: generator_utils.cmx
 generator_pr.cmi:
 generator_pr.cmo: generator_utils.cmi generator_pr.cmi
 generator_pr.cmx: generator_utils.cmx generator_pr.cmi
@@ -34,10 +36,12 @@ generator_checks.cmx: generator_utils.cmx
generator_types.cmx \
     generator_actions.cmx
 generator_c.cmo: generator_utils.cmi generator_types.cmo \
     generator_structs.cmi generator_pr.cmi generator_optgroups.cmo \
-    generator_docstrings.cmo generator_api_versions.cmi generator_actions.cmi
+    generator_events.cmo generator_docstrings.cmo generator_api_versions.cmi \
+    generator_actions.cmi
 generator_c.cmx: generator_utils.cmx generator_types.cmx \
     generator_structs.cmx generator_pr.cmx generator_optgroups.cmx \
-    generator_docstrings.cmx generator_api_versions.cmx generator_actions.cmx
+    generator_events.cmx generator_docstrings.cmx generator_api_versions.cmx \
+    generator_actions.cmx
 generator_xdr.cmo: generator_utils.cmi generator_types.cmo \
     generator_structs.cmi generator_pr.cmi generator_optgroups.cmo \
     generator_docstrings.cmo generator_actions.cmi
diff --git a/generator/Makefile.am b/generator/Makefile.am
index 39688c1..112fc69 100644
--- a/generator/Makefile.am
+++ b/generator/Makefile.am
@@ -28,6 +28,7 @@ SOURCES = \
 	generator_optgroups.ml \
 	generator_prepopts.mli \
 	generator_prepopts.ml \
+	generator_events.ml \
 	generator_pr.mli \
 	generator_pr.ml \
 	generator_docstrings.ml \
diff --git a/generator/generator_actions.ml b/generator/generator_actions.ml
index ca2dfe3..fa2f6f5 100644
--- a/generator/generator_actions.ml
+++ b/generator/generator_actions.ml
@@ -300,10 +300,14 @@ Get the autosync flag.");
    [],
    "set verbose mode",
    "\
-If C<verbose> is true, this turns on verbose messages (to
C<stderr>).
+If C<verbose> is true, this turns on verbose messages.

 Verbose messages are disabled unless the environment variable
-C<LIBGUESTFS_DEBUG> is defined and set to C<1>.");
+C<LIBGUESTFS_DEBUG> is defined and set to C<1>.
+
+Verbose messages are normally sent to C<stderr>, unless you
+register a callback to send them somewhere else (see
+C<guestfs_set_error_callback>).");

   ("get_verbose", (RBool "verbose", [], []), -1, [],
    [],
@@ -469,19 +473,19 @@ see L<guestfs(3)>.");
        ["get_trace"]])],
    "enable or disable command traces",
    "\
-If the command trace flag is set to 1, then commands are
-printed on stderr before they are executed in a format
-which is very similar to the one used by guestfish.  In
-other words, you can run a program with this enabled, and
-you will get out a script which you can feed to guestfish
-to perform the same set of actions.
+If the command trace flag is set to 1, then libguestfs
+calls, parameters and return values are traced.

 If you want to trace C API calls into libguestfs (and
 other libraries) then possibly a better way is to use
 the external ltrace(1) command.

 Command traces are disabled unless the environment variable
-C<LIBGUESTFS_TRACE> is defined and set to C<1>.");
+C<LIBGUESTFS_TRACE> is defined and set to C<1>.
+
+Trace messages are normally sent to C<stderr>, unless you
+register a callback to send them somewhere else (see
+C<guestfs_set_error_callback>).");

   ("get_trace", (RBool "trace", [], []), -1, [],
    [],
diff --git a/generator/generator_c.ml b/generator/generator_c.ml
index 9b88376..6fc1776 100644
--- a/generator/generator_c.ml
+++ b/generator/generator_c.ml
@@ -28,6 +28,7 @@ open Generator_api_versions
 open Generator_optgroups
 open Generator_actions
 open Generator_structs
+open Generator_events

 (* Generate C API. *)

@@ -396,6 +397,39 @@ extern void guestfs_set_out_of_memory_handler (guestfs_h
*g, guestfs_abort_cb);
 extern guestfs_abort_cb guestfs_get_out_of_memory_handler (guestfs_h *g);

 /* Events. */
+";
+
+  List.iter (
+    fun (name, bitmask) ->
+      pr "#define GUESTFS_EVENT_%-16s 0x%04x\n"
+        (String.uppercase name) bitmask
+  ) events;
+  pr "#define GUESTFS_EVENT_%-16s UINT64_MAX\n" "ALL";
+  pr "\n";
+
+  pr "\
+#ifndef GUESTFS_TYPEDEF_EVENT_CALLBACK
+#define GUESTFS_TYPEDEF_EVENT_CALLBACK 1
+typedef void (*guestfs_event_callback) (
+                        guestfs_h *g,
+                        void *opaque,
+                        uint64_t event,
+                        int event_handle,
+                        int flags,
+                        const char *buf, size_t buf_len,
+                        const uint64_t *array, size_t array_len);
+#endif
+
+#define LIBGUESTFS_HAVE_SET_EVENT_CALLBACK 1
+int guestfs_set_event_callback (guestfs_h *g,
+                                guestfs_event_callback cb,
+                                uint64_t event_bitmask,
+                                int flags,
+                                void *opaque);
+#define LIBGUESTFS_HAVE_DELETE_EVENT_CALLBACK 1
+void guestfs_delete_event_callback (guestfs_h *g, int event_handle);
+
+/* Old-style event handling.  In new code use guestfs_set_event_callback. */
 #ifndef GUESTFS_TYPEDEF_LOG_MESSAGE_CB
 #define GUESTFS_TYPEDEF_LOG_MESSAGE_CB 1
 typedef void (*guestfs_log_message_cb) (guestfs_h *g, void *opaque, char *buf,
int len);
@@ -580,6 +614,7 @@ and generate_client_actions ()  #include <unistd.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <assert.h>

 #include \"guestfs.h\"
 #include \"guestfs-internal.h\"
@@ -635,6 +670,42 @@ check_state (guestfs_h *g, const char *caller)
   return 0;
 }

+/* Convenience wrapper for tracing. */
+static FILE *
+trace_open (guestfs_h *g)
+{
+  assert (g->trace_fp == NULL);
+  g->trace_buf = NULL;
+  g->trace_len = 0;
+  g->trace_fp = open_memstream (&g->trace_buf, &g->trace_len);
+  if (g->trace_fp)
+    return g->trace_fp;
+  else
+    return stderr;
+}
+
+static void
+trace_send_line (guestfs_h *g)
+{
+  char *buf;
+  size_t len;
+
+  if (g->trace_fp) {
+    fclose (g->trace_fp);
+    g->trace_fp = NULL;
+
+    /* The callback might invoke other libguestfs calls, so keep
+     * a copy of the pointer to the buffer and length.
+     */
+    buf = g->trace_buf;
+    len = g->trace_len;
+    g->trace_buf = NULL;
+    guestfs___call_callbacks_message (g, GUESTFS_EVENT_TRACE, buf, len);
+
+    free (buf);
+  }
+}
+
 ";

   (* Generate code to check String-like parameters are not passed in
@@ -739,7 +810,9 @@ check_state (guestfs_h *g, const char *caller)
       pr "\n"
     );

-    pr "    fprintf (stderr, \"%%s: %%s: %%s\",\n";
+    pr "    trace_fp = trace_open (g);\n";
+
+    pr "    fprintf (trace_fp, \"%%s: %%s: %%s\",\n";
     pr "             \"libguestfs\", \"trace\",
\"%s\");\n" shortname;

     (* Required arguments. *)
@@ -752,33 +825,33 @@ check_state (guestfs_h *g, const char *caller)
       | FileIn n
       | FileOut n ->
           (* guestfish doesn't support string escaping, so neither do we *)
-          pr "    fprintf (stderr, \" \\\"%%s\\\"\",
%s);\n" n
+          pr "    fprintf (trace_fp, \" \\\"%%s\\\"\",
%s);\n" n
       | Key n ->
           (* don't print keys *)
-          pr "    fprintf (stderr, \"
\\\"***\\\"\");\n"
+          pr "    fprintf (trace_fp, \"
\\\"***\\\"\");\n"
       | OptString n ->			(* string option *)
-          pr "    if (%s) fprintf (stderr, \"
\\\"%%s\\\"\", %s);\n" n n;
-          pr "    else fprintf (stderr, \" null\");\n"
+          pr "    if (%s) fprintf (trace_fp, \"
\\\"%%s\\\"\", %s);\n" n n;
+          pr "    else fprintf (trace_fp, \" null\");\n"
       | StringList n
       | DeviceList n ->			(* string list *)
-          pr "    fputc (' ', stderr);\n";
-          pr "    fputc ('\"', stderr);\n";
+          pr "    fputc (' ', trace_fp);\n";
+          pr "    fputc ('\"', trace_fp);\n";
           pr "    for (i = 0; %s[i]; ++i) {\n" n;
-          pr "      if (i > 0) fputc (' ', stderr);\n";
-          pr "      fputs (%s[i], stderr);\n" n;
+          pr "      if (i > 0) fputc (' ', trace_fp);\n";
+          pr "      fputs (%s[i], trace_fp);\n" n;
           pr "    }\n";
-          pr "    fputc ('\"', stderr);\n";
+          pr "    fputc ('\"', trace_fp);\n";
       | Bool n ->			(* boolean *)
-          pr "    fputs (%s ? \" true\" : \" false\",
stderr);\n" n
+          pr "    fputs (%s ? \" true\" : \" false\",
trace_fp);\n" n
       | Int n ->			(* int *)
-          pr "    fprintf (stderr, \" %%d\", %s);\n" n
+          pr "    fprintf (trace_fp, \" %%d\", %s);\n" n
       | Int64 n ->
-          pr "    fprintf (stderr, \" %%\" PRIi64, %s);\n"
n
+          pr "    fprintf (trace_fp, \" %%\" PRIi64,
%s);\n" n
       | BufferIn n ->                   (* RHBZ#646822 *)
-          pr "    fputc (' ', stderr);\n";
-          pr "    guestfs___print_BufferIn (stderr, %s, %s_size);\n"
n n
+          pr "    fputc (' ', trace_fp);\n";
+          pr "    guestfs___print_BufferIn (trace_fp, %s,
%s_size);\n" n n
       | Pointer (t, n) ->
-          pr "    fprintf (stderr, \" (%s)%%p\", %s);\n" t
n
+          pr "    fprintf (trace_fp, \" (%s)%%p\", %s);\n"
t n
     ) args;

     (* Optional arguments. *)
@@ -791,18 +864,19 @@ check_state (guestfs_h *g, const char *caller)
           uc_shortname uc_n;
         (match argt with
          | String n ->
-             pr "      fprintf (stderr, \"
\\\"%%s:%%s\\\"\", \"%s\", optargs->%s);\n" n n
+             pr "      fprintf (trace_fp, \"
\\\"%%s:%%s\\\"\", \"%s\", optargs->%s);\n" n n
          | Bool n ->
-             pr "      fprintf (stderr, \"
\\\"%%s:%%s\\\"\", \"%s\", optargs->%s ?
\"true\" : \"false\");\n" n n
+             pr "      fprintf (trace_fp, \"
\\\"%%s:%%s\\\"\", \"%s\", optargs->%s ?
\"true\" : \"false\");\n" n n
          | Int n ->
-             pr "      fprintf (stderr, \"
\\\"%%s:%%d\\\"\", \"%s\", optargs->%s);\n" n n
+             pr "      fprintf (trace_fp, \"
\\\"%%s:%%d\\\"\", \"%s\", optargs->%s);\n" n n
          | Int64 n ->
-             pr "      fprintf (stderr, \" \\\"%%s:%%\"
PRIi64 \"\\\"\", \"%s\", optargs->%s);\n" n n
+             pr "      fprintf (trace_fp, \" \\\"%%s:%%\"
PRIi64 \"\\\"\", \"%s\", optargs->%s);\n" n n
          | _ -> assert false
         );
     ) optargs;

-    pr "    fputc ('\\n', stderr);\n";
+    pr "    fputc ('\\n', trace_fp);\n";
+    pr "    trace_send_line (g);\n";
     pr "  }\n";
     pr "\n";
   in
@@ -821,67 +895,56 @@ check_state (guestfs_h *g, const char *caller)
       pr "\n"
     );

-    pr "%s  fprintf (stderr, \"%%s: %%s: %%s = \",\n"
indent;
+    pr "%s  trace_fp = trace_open (g);\n" indent;
+
+    pr "%s  fprintf (trace_fp, \"%%s: %%s: %%s = \",\n"
indent;
     pr "%s           \"libguestfs\", \"trace\",
\"%s\");\n"
       indent shortname;

     (match ret with
      | RErr | RInt _ | RBool _ ->
-         pr "%s  fprintf (stderr, \"%%d\", %s);\n" indent
rv
+         pr "%s  fprintf (trace_fp, \"%%d\", %s);\n" indent
rv
      | RInt64 _ ->
-         pr "%s  fprintf (stderr, \"%%\" PRIi64, %s);\n"
indent rv
+         pr "%s  fprintf (trace_fp, \"%%\" PRIi64, %s);\n"
indent rv
      | RConstString _ | RString _ ->
-         pr "%s  fprintf (stderr, \"\\\"%%s\\\"\",
%s);\n" indent rv
+         pr "%s  fprintf (trace_fp, \"\\\"%%s\\\"\",
%s);\n" indent rv
      | RConstOptString _ ->
-         pr "%s  fprintf (stderr, \"\\\"%%s\\\"\", %s
!= NULL ? %s : \"NULL\");\n"
+         pr "%s  fprintf (trace_fp, \"\\\"%%s\\\"\",
%s != NULL ? %s : \"NULL\");\n"
            indent rv rv
      | RBufferOut _ ->
-         pr "%s  guestfs___print_BufferOut (stderr, %s, *size_r);\n"
indent rv
+         pr "%s  guestfs___print_BufferOut (trace_fp, %s,
*size_r);\n" indent rv
      | RStringList _ | RHashtable _ ->
-         pr "%s  fputs (\"[\\\"\", stderr);\n" indent;
+         pr "%s  fputs (\"[\\\"\", trace_fp);\n"
indent;
          pr "%s  for (i = 0; %s[i]; ++i) {\n" indent rv;
-         pr "%s    if (i > 0) fputs (\"\\\",
\\\"\", stderr);\n" indent;
-         pr "%s    fputs (%s[i], stderr);\n" indent rv;
+         pr "%s    if (i > 0) fputs (\"\\\",
\\\"\", trace_fp);\n" indent;
+         pr "%s    fputs (%s[i], trace_fp);\n" indent rv;
          pr "%s  }\n" indent;
-         pr "%s  fputs (\"\\\"]\", stderr);\n" indent;
+         pr "%s  fputs (\"\\\"]\", trace_fp);\n"
indent;
      | RStruct (_, typ) ->
          (* XXX There is code generated for guestfish for printing
           * these structures.  We need to make it generally available
           * for all callers
           *)
-         pr "%s  fprintf (stderr, \"<struct guestfs_%s
*>\");\n"
+         pr "%s  fprintf (trace_fp, \"<struct guestfs_%s
*>\");\n"
            indent typ (* XXX *)
      | RStructList (_, typ) ->
-         pr "%s  fprintf (stderr, \"<struct guestfs_%s_list
*>\");\n"
+         pr "%s  fprintf (trace_fp, \"<struct guestfs_%s_list
*>\");\n"
            indent typ (* XXX *)
     );
-    pr "%s  fputc ('\\n', stderr);\n" indent;
+    pr "%s  fputc ('\\n', trace_fp);\n" indent;
+    pr "%s  trace_send_line (g);\n" indent;
     pr "%s}\n" indent;
     pr "\n";
   in

-  let trace_return_error ?(indent = 2) shortname (ret, _, _) +  let
trace_return_error ?(indent = 2) shortname (ret, _, _) errcode      let indent =
spaces indent in

     pr "%sif (trace_flag)\n" indent;

-    pr "%s  fprintf (stderr, \"%%s: %%s: %%s = %%s
(error)\\n\",\n" indent;
-    pr "%s           \"libguestfs\", \"trace\",
\"%s\", "
-      indent shortname;
-
-    (match ret with
-     | RErr | RInt _ | RBool _
-     | RInt64 _ ->
-         pr "\"-1\""
-     | RConstString _ | RString _
-     | RConstOptString _
-     | RBufferOut _
-     | RStringList _ | RHashtable _
-     | RStruct _
-     | RStructList _ ->
-         pr "\"NULL\""
-    );
-    pr ");\n"
+    pr "%s  guestfs___trace (g, \"%%s: %%s: %%s = %%s
(error)\\n\",\n" indent;
+    pr "%s                   \"libguestfs\",
\"trace\", \"%s\", \"%s\");\n"
+      indent shortname (string_of_errcode errcode)
   in

   (* For non-daemon functions, generate a wrapper around each function. *)
@@ -897,6 +960,7 @@ check_state (guestfs_h *g, const char *caller)
           shortname style;
       pr "{\n";
       pr "  int trace_flag = g->trace;\n";
+      pr "  FILE *trace_fp;\n";
       (match ret with
        | RErr | RInt _ | RBool _ ->
            pr "  int r;\n"
@@ -928,7 +992,7 @@ check_state (guestfs_h *g, const char *caller)
            pr "  if (r != %s) {\n" (string_of_errcode errcode);
            trace_return ~indent:4 shortname style "r";
            pr "  } else {\n";
-           trace_return_error ~indent:4 shortname style;
+           trace_return_error ~indent:4 shortname style errcode;
            pr "  }\n";
        | `CannotReturnError ->
            trace_return shortname style "r";
@@ -981,6 +1045,7 @@ check_state (guestfs_h *g, const char *caller)
       pr "  int serial;\n";
       pr "  int r;\n";
       pr "  int trace_flag = g->trace;\n";
+      pr "  FILE *trace_fp;\n";
       (match ret with
        | RErr | RInt _ | RBool _ ->
            pr "  int ret_v;\n"
@@ -1026,7 +1091,7 @@ check_state (guestfs_h *g, const char *caller)

       (* Check we are in the right state for sending a request. *)
       pr "  if (check_state (g, \"%s\") == -1) {\n"
shortname;
-      trace_return_error ~indent:4 shortname style;
+      trace_return_error ~indent:4 shortname style errcode;
       pr "    return %s;\n" (string_of_errcode errcode);
       pr "  }\n";
       pr "  guestfs___set_busy (g);\n";
@@ -1057,7 +1122,7 @@ check_state (guestfs_h *g, const char *caller)
           | BufferIn n ->
               pr "  /* Just catch grossly large sizes. XDR encoding will
make this precise. */\n";
               pr "  if (%s_size >= GUESTFS_MESSAGE_MAX) {\n" n;
-              trace_return_error ~indent:4 shortname style;
+              trace_return_error ~indent:4 shortname style errcode;
               pr "    error (g, \"%%s: size of input buffer too
large\", \"%s\");\n"
                 shortname;
               pr "    guestfs___end_busy (g);\n";
@@ -1099,7 +1164,7 @@ check_state (guestfs_h *g, const char *caller)
       );
       pr "  if (serial == -1) {\n";
       pr "    guestfs___end_busy (g);\n";
-      trace_return_error ~indent:4 shortname style;
+      trace_return_error ~indent:4 shortname style errcode;
       pr "    return %s;\n" (string_of_errcode errcode);
       pr "  }\n";
       pr "\n";
@@ -1112,7 +1177,7 @@ check_state (guestfs_h *g, const char *caller)
             pr "  r = guestfs___send_file (g, %s);\n" n;
             pr "  if (r == -1) {\n";
             pr "    guestfs___end_busy (g);\n";
-            trace_return_error ~indent:4 shortname style;
+            trace_return_error ~indent:4 shortname style errcode;
             pr "    return %s;\n" (string_of_errcode errcode);
             pr "  }\n";
             pr "  if (r == -2) /* daemon cancelled */\n";
@@ -1137,7 +1202,7 @@ check_state (guestfs_h *g, const char *caller)

       pr "  if (r == -1) {\n";
       pr "    guestfs___end_busy (g);\n";
-      trace_return_error ~indent:4 shortname style;
+      trace_return_error ~indent:4 shortname style errcode;
       pr "    return %s;\n" (string_of_errcode errcode);
       pr "  }\n";
       pr "\n";
@@ -1145,13 +1210,13 @@ check_state (guestfs_h *g, const char *caller)
       pr "  if (check_reply_header (g, &hdr, GUESTFS_PROC_%s, serial)
== -1) {\n"
         (String.uppercase shortname);
       pr "    guestfs___end_busy (g);\n";
-      trace_return_error ~indent:4 shortname style;
+      trace_return_error ~indent:4 shortname style errcode;
       pr "    return %s;\n" (string_of_errcode errcode);
       pr "  }\n";
       pr "\n";

       pr "  if (hdr.status == GUESTFS_STATUS_ERROR) {\n";
-      trace_return_error ~indent:4 shortname style;
+      trace_return_error ~indent:4 shortname style errcode;
       pr "    int errnum = 0;\n";
       pr "    if (err.errno_string[0] != '\\0')\n";
       pr "      errnum = guestfs___string_to_errno
(err.errno_string);\n";
@@ -1175,7 +1240,7 @@ check_state (guestfs_h *g, const char *caller)
         | FileOut n ->
             pr "  if (guestfs___recv_file (g, %s) == -1) {\n" n;
             pr "    guestfs___end_busy (g);\n";
-            trace_return_error ~indent:4 shortname style;
+            trace_return_error ~indent:4 shortname style errcode;
             pr "    return %s;\n" (string_of_errcode errcode);
             pr "  }\n";
             pr "\n";
@@ -1359,6 +1424,7 @@ and generate_linker_script ()    let globals = [
     "guestfs_create";
     "guestfs_close";
+    "guestfs_delete_event_callback";
     "guestfs_get_error_handler";
     "guestfs_get_out_of_memory_handler";
     "guestfs_get_private";
@@ -1366,6 +1432,7 @@ and generate_linker_script ()
"guestfs_last_error";
     "guestfs_set_close_callback";
     "guestfs_set_error_handler";
+    "guestfs_set_event_callback";
     "guestfs_set_launch_done_callback";
     "guestfs_set_log_message_callback";
     "guestfs_set_out_of_memory_handler";
diff --git a/generator/generator_events.ml b/generator/generator_events.ml
new file mode 100644
index 0000000..54557c3
--- /dev/null
+++ b/generator/generator_events.ml
@@ -0,0 +1,40 @@
+(* libguestfs
+ * Copyright (C) 2011 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *)
+
+(* Please read generator/README first. *)
+
+open Generator_utils
+
+(* NB: DO NOT REORDER THESE, as doing so will change the ABI.  Only
+ * add new event types at the end of the list.
+ *)
+let events = [
+  "close";                              (* close handle *)
+  "subprocess_quit";                    (* subprocess quit *)
+  "launch_done";                        (* launched *)
+
+  "progress";                           (* progress message *)
+
+  (* log messages from various sources *)
+  "appliance";                          (* log messages from
+                                           qemu / kernel / guestfsd / tools *)
+  "library";                            (* log messages from library
*)
+  "trace";                              (* call trace messages *)
+]
+
+let events = mapi (fun i name -> name, 1 lsl i) events
diff --git a/po/POTFILES.in b/po/POTFILES.in
index fd8778a..afe4798 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -133,6 +133,7 @@ src/appliance.c
 src/bindtests.c
 src/errnostring.c
 src/errnostring_gperf.c
+src/events.c
 src/filearch.c
 src/guestfs.c
 src/inspect.c
diff --git a/src/Makefile.am b/src/Makefile.am
index 2b9c49b..3e1201d 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -127,6 +127,7 @@ libguestfs_la_SOURCES = \
 	actions.c \
 	appliance.c \
 	bindtests.c \
+	events.c \
 	filearch.c \
 	inspect.c \
 	launch.c \
diff --git a/src/appliance.c b/src/appliance.c
index ef724be..0bcda89 100644
--- a/src/appliance.c
+++ b/src/appliance.c
@@ -1,5 +1,5 @@
 /* libguestfs
- * Copyright (C) 2010 Red Hat Inc.
+ * Copyright (C) 2010-2011 Red Hat Inc.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -241,14 +241,14 @@ calculate_supermin_checksum (guestfs_h *g, const char
*supermin_path)
   }

   if (pclose (pp) == -1) {
-    perror ("pclose");
+    warning (g, "pclose: %m");
     return NULL;
   }

   len = strlen (checksum);

   if (len < 16) {               /* sanity check */
-    fprintf (stderr, "libguestfs: internal error:
febootstrap-supermin-helper -f checksum returned a short string\n");
+    warning (g, "febootstrap-supermin-helper -f checksum returned a short
string");
     return NULL;
   }

diff --git a/src/events.c b/src/events.c
new file mode 100644
index 0000000..d3e3b71
--- /dev/null
+++ b/src/events.c
@@ -0,0 +1,301 @@
+/* libguestfs
+ * Copyright (C) 2011 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <config.h>
+
+#define _BSD_SOURCE /* for mkdtemp, usleep */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <unistd.h>
+#include <assert.h>
+
+#include "ignore-value.h"
+
+#include "guestfs.h"
+#include "guestfs-internal.h"
+
+int
+guestfs_set_event_callback (guestfs_h *g,
+                            guestfs_event_callback cb,
+                            uint64_t event_bitmask,
+                            int flags,
+                            void *opaque)
+{
+  if (flags != 0) {
+    error (g, "flags parameter should be passed as 0 to this
function");
+    return -1;
+  }
+
+  /* We cast size_t to int which is not always safe for large numbers,
+   * and in any case if a program is registering a huge number of
+   * callbacks then we'd want to look at using an alternate data
+   * structure in place of a linear list.
+   */
+  if (g->nr_events >= 1000) {
+    error (g, "too many event callbacks registered");
+    return -1;
+  }
+
+  int event_handle = (int) g->nr_events;
+  g->events +    guestfs_safe_realloc (g, g->events,
+                          (g->nr_events+1) * sizeof (struct event));
+  g->nr_events++;
+
+  g->events[event_handle].event_bitmask = event_bitmask;
+  g->events[event_handle].cb = cb;
+  g->events[event_handle].opaque = opaque;
+  g->events[event_handle].opaque2 = NULL;
+
+  return event_handle;
+}
+
+void
+guestfs_delete_event_callback (guestfs_h *g, int event_handle)
+{
+  if (event_handle < 0 || event_handle >= (int) g->nr_events)
+    return;
+
+  /* Set the event_bitmask to 0, which will ensure that this callback
+   * cannot match any event and therefore cannot be called.
+   */
+  g->events[event_handle].event_bitmask = 0;
+}
+
+/* Functions to generate an event with various payloads. */
+
+void
+guestfs___call_callbacks_void (guestfs_h *g, uint64_t event)
+{
+  size_t i;
+
+  for (i = 0; i < g->nr_events; ++i)
+    if ((g->events[i].event_bitmask & event) != 0)
+      g->events[i].cb (g, g->events[i].opaque, event, i, 0, NULL, 0,
NULL, 0);
+
+  /* All events with payload type void are discarded if no callback
+   * was registered.
+   */
+}
+
+void
+guestfs___call_callbacks_message (guestfs_h *g, uint64_t event,
+                                  const char *buf, size_t buf_len)
+{
+  size_t i, count = 0;
+
+  for (i = 0; i < g->nr_events; ++i)
+    if ((g->events[i].event_bitmask & event) != 0) {
+      g->events[i].cb (g, g->events[i].opaque, event, i, 0,
+                       buf, buf_len, NULL, 0);
+      count++;
+    }
+
+  /* If nothing was registered and we're verbose or tracing, then we
+   * print the message on stderr.  This essentially emulates the
+   * behaviour of the old-style handlers, while allowing callers to
+   * override print-on-stderr simply by registering a callback.
+   */
+  if (count == 0 && (g->verbose || event == GUESTFS_EVENT_TRACE))
+    ignore_value (write (STDERR_FILENO, buf, buf_len));
+}
+
+void
+guestfs___call_callbacks_array (guestfs_h *g, uint64_t event,
+                                const uint64_t *array, size_t array_len)
+{
+  size_t i;
+
+  for (i = 0; i < g->nr_events; ++i)
+    if ((g->events[i].event_bitmask & event) != 0)
+      g->events[i].cb (g, g->events[i].opaque, event, i, 0,
+                       NULL, 0, array, array_len);
+
+  /* All events with payload type array are discarded if no callback
+   * was registered.
+   */
+}
+
+/* Emulate old-style callback API.
+ *
+ * There were no event handles, so multiple callbacks per event were
+ * not supported.  Calling the same 'guestfs_set_*_callback' function
+ * would replace the existing event.  Calling it with cb == NULL meant
+ * that the caller wanted to remove the callback.
+ */
+
+static void
+replace_old_style_event_callback (guestfs_h *g,
+                                  guestfs_event_callback cb,
+                                  uint64_t event_bitmask,
+                                  void *opaque,
+                                  void *opaque2)
+{
+  size_t i;
+
+  /* Use 'cb' pointer as a sentinel to replace the existing callback
+   * for this event if one was registered previously.  Else append a
+   * new event.
+   */
+
+  for (i = 0; i < g->nr_events; ++i)
+    if (g->events[i].cb == cb) {
+      if (opaque2 == NULL) {
+        /* opaque2 (the original callback) is NULL, which in the
+         * old-style API meant remove the callback.
+         */
+        guestfs_delete_event_callback (g, i);
+        return;
+      }
+
+      goto replace;
+    }
+
+  if (opaque2 == NULL)
+    return; /* see above */
+
+  /* i == g->nr_events */
+  g->events +    guestfs_safe_realloc (g, g->events,
+                          (g->nr_events+1) * sizeof (struct event));
+  g->nr_events++;
+
+ replace:
+  g->events[i].event_bitmask = event_bitmask;
+  g->events[i].cb = cb;
+  g->events[i].opaque = opaque;
+  g->events[i].opaque2 = opaque2;
+}
+
+static void
+log_message_callback_wrapper (guestfs_h *g,
+                              void *opaque,
+                              uint64_t event,
+                              int event_handle,
+                              int flags,
+                              const char *buf, size_t buf_len,
+                              const uint64_t *array, size_t array_len)
+{
+  guestfs_log_message_cb cb = g->events[event_handle].opaque2;
+  /* Note that the old callback declared the message buffer as
+   * (char *, int).  I sure hope message buffers aren't too large
+   * and that callers aren't writing to them. XXX
+   */
+  cb (g, opaque, (char *) buf, (int) buf_len);
+}
+
+void
+guestfs_set_log_message_callback (guestfs_h *g,
+                                  guestfs_log_message_cb cb, void *opaque)
+{
+  replace_old_style_event_callback (g, log_message_callback_wrapper,
+                                    GUESTFS_EVENT_APPLIANCE,
+                                    opaque, cb);
+}
+
+static void
+subprocess_quit_callback_wrapper (guestfs_h *g,
+                                  void *opaque,
+                                  uint64_t event,
+                                  int event_handle,
+                                  int flags,
+                                  const char *buf, size_t buf_len,
+                                  const uint64_t *array, size_t array_len)
+{
+  guestfs_subprocess_quit_cb cb = g->events[event_handle].opaque2;
+  cb (g, opaque);
+}
+
+void
+guestfs_set_subprocess_quit_callback (guestfs_h *g,
+                                      guestfs_subprocess_quit_cb cb, void
*opaque)
+{
+  replace_old_style_event_callback (g, subprocess_quit_callback_wrapper,
+                                    GUESTFS_EVENT_SUBPROCESS_QUIT,
+                                    opaque, cb);
+}
+
+static void
+launch_done_callback_wrapper (guestfs_h *g,
+                              void *opaque,
+                              uint64_t event,
+                              int event_handle,
+                              int flags,
+                              const char *buf, size_t buf_len,
+                              const uint64_t *array, size_t array_len)
+{
+  guestfs_launch_done_cb cb = g->events[event_handle].opaque2;
+  cb (g, opaque);
+}
+
+void
+guestfs_set_launch_done_callback (guestfs_h *g,
+                                  guestfs_launch_done_cb cb, void *opaque)
+{
+  replace_old_style_event_callback (g, launch_done_callback_wrapper,
+                                    GUESTFS_EVENT_LAUNCH_DONE,
+                                    opaque, cb);
+}
+
+static void
+close_callback_wrapper (guestfs_h *g,
+                        void *opaque,
+                        uint64_t event,
+                        int event_handle,
+                        int flags,
+                        const char *buf, size_t buf_len,
+                        const uint64_t *array, size_t array_len)
+{
+  guestfs_close_cb cb = g->events[event_handle].opaque2;
+  cb (g, opaque);
+}
+
+void
+guestfs_set_close_callback (guestfs_h *g,
+                            guestfs_close_cb cb, void *opaque)
+{
+  replace_old_style_event_callback (g, close_callback_wrapper,
+                                    GUESTFS_EVENT_CLOSE,
+                                    opaque, cb);
+}
+
+static void
+progress_callback_wrapper (guestfs_h *g,
+                           void *opaque,
+                           uint64_t event,
+                           int event_handle,
+                           int flags,
+                           const char *buf, size_t buf_len,
+                           const uint64_t *array, size_t array_len)
+{
+  guestfs_progress_cb cb = g->events[event_handle].opaque2;
+  assert (array_len >= 4);
+  cb (g, opaque, (int) array[0], (int) array[1], array[2], array[3]);
+}
+
+void
+guestfs_set_progress_callback (guestfs_h *g,
+                               guestfs_progress_cb cb, void *opaque)
+{
+  replace_old_style_event_callback (g, progress_callback_wrapper,
+                                    GUESTFS_EVENT_PROGRESS,
+                                    opaque, cb);
+}
diff --git a/src/guestfs-internal.h b/src/guestfs-internal.h
index 0eb395b..077c5f9 100644
--- a/src/guestfs-internal.h
+++ b/src/guestfs-internal.h
@@ -87,6 +87,18 @@ enum state { CONFIG, LAUNCHING, READY, BUSY, NO_HANDLE };
 /* Attach method. */
 enum attach_method { ATTACH_METHOD_APPLIANCE = 0, ATTACH_METHOD_UNIX };

+/* Event. */
+struct event {
+  uint64_t event_bitmask;
+  guestfs_event_callback cb;
+  void *opaque;
+
+  /* opaque2 is not exposed through the API, but is used internally to
+   * emulate the old-style callback API.
+   */
+  void *opaque2;
+};
+
 struct guestfs_h
 {
   struct guestfs_h *next;	/* Linked list of open handles. */
@@ -133,16 +145,10 @@ struct guestfs_h
   guestfs_abort_cb           abort_cb;
   guestfs_error_handler_cb   error_cb;
   void *                     error_cb_data;
-  guestfs_log_message_cb     log_message_cb;
-  void *                     log_message_cb_data;
-  guestfs_subprocess_quit_cb subprocess_quit_cb;
-  void *                     subprocess_quit_cb_data;
-  guestfs_launch_done_cb     launch_done_cb;
-  void *                     launch_done_cb_data;
-  guestfs_close_cb           close_cb;
-  void *                     close_cb_data;
-  guestfs_progress_cb        progress_cb;
-  void *                     progress_cb_data;
+
+  /* Events. */
+  struct event *events;
+  size_t nr_events;

   int msg_next_serial;

@@ -154,6 +160,11 @@ struct guestfs_h

   /* Private data area. */
   struct hash_table *pda;
+
+  /* Used by src/actions.c:trace_* functions. */
+  FILE *trace_fp;
+  char *trace_buf;
+  size_t trace_len;
 };

 /* Per-filesystem data stored for inspect_os. */
@@ -263,6 +274,12 @@ extern char *guestfs_safe_strndup (guestfs_h *g, const char
*str, size_t n);
 extern void *guestfs_safe_memdup (guestfs_h *g, void *ptr, size_t size);
 extern char *guestfs_safe_asprintf (guestfs_h *g, const char *fs, ...)
   __attribute__((format (printf,2,3)));
+extern void guestfs___warning (guestfs_h *g, const char *fs, ...)
+  __attribute__((format (printf,2,3)));
+extern void guestfs___debug (guestfs_h *g, const char *fs, ...)
+  __attribute__((format (printf,2,3)));
+extern void guestfs___trace (guestfs_h *g, const char *fs, ...)
+  __attribute__((format (printf,2,3)));
 extern const char *guestfs___persistent_tmpdir (void);
 extern void guestfs___print_timestamped_argv (guestfs_h *g, const char
*argv[]);
 extern void guestfs___print_timestamped_message (guestfs_h *g, const char *fs,
...);
@@ -289,9 +306,15 @@ extern int guestfs___feature_available (guestfs_h *g, const
char *feature);
 extern void guestfs___free_string_list (char **);
 extern int guestfs___checkpoint_cmdline (guestfs_h *g);
 extern void guestfs___rollback_cmdline (guestfs_h *g, int pos);
+extern void guestfs___call_callbacks_void (guestfs_h *g, uint64_t event);
+extern void guestfs___call_callbacks_message (guestfs_h *g, uint64_t event,
const char *buf, size_t buf_len);
+extern void guestfs___call_callbacks_array (guestfs_h *g, uint64_t event, const
uint64_t *array, size_t array_len);

 #define error(g,...) guestfs_error_errno((g),0,__VA_ARGS__)
 #define perrorf guestfs_perrorf
+#define warning(g,...) guestfs___warning((g),__VA_ARGS__)
+#define debug(g,...) \
+  do { if ((g)->verbose) guestfs___debug ((g),__VA_ARGS__); } while (0)
 #define safe_calloc guestfs_safe_calloc
 #define safe_malloc guestfs_safe_malloc
 #define safe_realloc guestfs_safe_realloc
diff --git a/src/guestfs.c b/src/guestfs.c
index 8b7ab4d..89d46b9 100644
--- a/src/guestfs.c
+++ b/src/guestfs.c
@@ -132,7 +132,7 @@ guestfs_create (void)
   str = getenv ("LIBGUESTFS_MEMSIZE");
   if (str) {
     if (sscanf (str, "%d", &g->memsize) != 1 || g->memsize
<= 256) {
-      fprintf (stderr, "libguestfs: non-numeric or too small value for
LIBGUESTFS_MEMSIZE\n");
+      warning (g, "non-numeric or too small value for
LIBGUESTFS_MEMSIZE");
       goto error;
     }
   } else
@@ -153,8 +153,7 @@ guestfs_create (void)
   }
   gl_lock_unlock (handles_lock);

-  if (g->verbose)
-    fprintf (stderr, "new guestfs handle %p\n", g);
+  debug (g, "new guestfs handle %p", g);

   return g;

@@ -174,32 +173,33 @@ guestfs_close (guestfs_h *g)
   guestfs_h *gg;

   if (g->state == NO_HANDLE) {
-    /* Not safe to call 'error' here, so ... */
+    /* Not safe to call ANY callbacks here, so ... */
     fprintf (stderr, _("guestfs_close: called twice on the same
handle\n"));
     return;
   }

-  if (g->verbose)
-    fprintf (stderr, "closing guestfs handle %p (state %d)\n", g,
g->state);
-
-  /* Run user close callback before anything else. */
-  if (g->close_cb)
-    g->close_cb (g, g->close_cb_data);
-
-  guestfs___free_inspect_info (g);
+  debug (g, "closing guestfs handle %p (state %d)", g, g->state);

   /* Try to sync if autosync flag is set. */
   if (g->autosync && g->state == READY)
     guestfs_internal_autosync (g);

-  /* Remove any handlers that might be called back before we kill the
-   * subprocess.
-   */
-  g->log_message_cb = NULL;
-
+  /* Kill the qemu subprocess. */
   if (g->state != CONFIG)
     guestfs_kill_subprocess (g);

+  /* Run user close callbacks. */
+  guestfs___call_callbacks_void (g, GUESTFS_EVENT_CLOSE);
+
+  /* Remove all other registered callbacks.  Since we've already
+   * called the close callbacks, we shouldn't call any others.
+   */
+  free (g->events);
+  g->nr_events = 0;
+  g->events = NULL;
+
+  guestfs___free_inspect_info (g);
+
   /* Close sockets. */
   if (g->fd[0] >= 0)
     close (g->fd[0]);
@@ -282,6 +282,86 @@ set_last_error (guestfs_h *g, int errnum, const char *msg)
   g->last_errnum = errnum;
 }

+/* Warning are printed unconditionally.  We try to make these rare.
+ * Generally speaking, a warning should either be an error, or if it's
+ * not important for end users then it should be a debug message.
+ */
+void
+guestfs___warning (guestfs_h *g, const char *fs, ...)
+{
+  va_list args;
+  char *msg, *msg2;
+  int len;
+
+  va_start (args, fs);
+  len = vasprintf (&msg, fs, args);
+  va_end (args);
+
+  if (len < 0) return;
+
+  len = asprintf (&msg2, "libguestfs: warning: %s\n", msg);
+  free (msg);
+
+  if (len < 0) return;
+
+  guestfs___call_callbacks_message (g, GUESTFS_EVENT_LIBRARY, msg2, len);
+
+  free (msg2);
+}
+
+/* Debug messages. */
+void
+guestfs___debug (guestfs_h *g, const char *fs, ...)
+{
+  va_list args;
+  char *msg, *msg2;
+  int len;
+
+  /* The cpp macro "debug" has already checked that g->verbose is
true
+   * before calling this function, but we check it again just in case
+   * anyone calls this function directly.
+   */
+  if (!g->verbose)
+    return;
+
+  va_start (args, fs);
+  len = vasprintf (&msg, fs, args);
+  va_end (args);
+
+  if (len < 0) return;
+
+  len = asprintf (&msg2, "libguestfs: %s\n", msg);
+  free (msg);
+
+  if (len < 0) return;
+
+  guestfs___call_callbacks_message (g, GUESTFS_EVENT_LIBRARY, msg2, len);
+
+  free (msg2);
+}
+
+/* Call trace messages.  These are enabled by setting g->trace, and
+ * calls to this function should only happen from the generated code
+ * in src/actions.c
+ */
+void
+guestfs___trace (guestfs_h *g, const char *fs, ...)
+{
+  va_list args;
+  char *msg;
+  int len;
+
+  va_start (args, fs);
+  len = vasprintf (&msg, fs, args);
+  va_end (args);
+
+  if (len < 0) return;
+
+  guestfs___call_callbacks_message (g, GUESTFS_EVENT_TRACE, msg, len);
+
+  free (msg);
+}
+
 static void
 default_error_cb (guestfs_h *g, void *data, const char *msg)
 {
@@ -690,46 +770,6 @@ guestfs__get_attach_method (guestfs_h *g)
   return ret;
 }

-void
-guestfs_set_log_message_callback (guestfs_h *g,
-                                  guestfs_log_message_cb cb, void *opaque)
-{
-  g->log_message_cb = cb;
-  g->log_message_cb_data = opaque;
-}
-
-void
-guestfs_set_subprocess_quit_callback (guestfs_h *g,
-                                      guestfs_subprocess_quit_cb cb, void
*opaque)
-{
-  g->subprocess_quit_cb = cb;
-  g->subprocess_quit_cb_data = opaque;
-}
-
-void
-guestfs_set_launch_done_callback (guestfs_h *g,
-                                  guestfs_launch_done_cb cb, void *opaque)
-{
-  g->launch_done_cb = cb;
-  g->launch_done_cb_data = opaque;
-}
-
-void
-guestfs_set_close_callback (guestfs_h *g,
-                            guestfs_close_cb cb, void *opaque)
-{
-  g->close_cb = cb;
-  g->close_cb_data = opaque;
-}
-
-void
-guestfs_set_progress_callback (guestfs_h *g,
-                               guestfs_progress_cb cb, void *opaque)
-{
-  g->progress_cb = cb;
-  g->progress_cb_data = opaque;
-}
-
 /* Note the private data area is allocated lazily, since the vast
  * majority of callers will never use it.  This means g->pda is
  * likely to be NULL.
diff --git a/src/guestfs.pod b/src/guestfs.pod
index 0b3b654..3601321 100644
--- a/src/guestfs.pod
+++ b/src/guestfs.pod
@@ -1683,82 +1683,71 @@ For guestfish, see L<guestfish(1)/OPTIONAL
ARGUMENTS>.

 =head2 SETTING CALLBACKS TO HANDLE EVENTS

-The child process generates events in some situations.  Current events
-include: receiving a log message, the child process exits.
-
-Use the C<guestfs_set_*_callback> functions to set a callback for
-different types of events.
-
-Only I<one callback of each type> can be registered for each handle.
-Calling C<guestfs_set_*_callback> again overwrites the previous
-callback of that type.  Cancel all callbacks of this type by calling
-this function with C<cb> set to C<NULL>.
-
-=head2 guestfs_set_log_message_callback
+B<Note:> This section documents the new-style event mechanism, which
+you should use in new code if possible.  The old functions
+C<guestfs_set_log_message_callback>,
+C<guestfs_set_subprocess_quit_callback>,
+C<guestfs_set_launch_done_callback>, C<guestfs_set_close_callback>
and
+C<guestfs_set_progress_callback> are no longer documented in this
+manual page.

- typedef void (*guestfs_log_message_cb) (guestfs_h *g, void *opaque,
-                                         char *buf, int len);
- void guestfs_set_log_message_callback (guestfs_h *g,
-                                        guestfs_log_message_cb cb,
-                                        void *opaque);
+Handles generate events when certain things happen, such as log
+messages being generated, progress messages during long-running
+operations, or the handle being closed.  The API calls described below
+let you register a callback to be called when events happen.  You can
+register multiple callbacks (for the same, different or overlapping
+sets of events), and individually remove callbacks.  If callbacks are
+not removed, then they remain in force until the handle is closed.

-The callback function C<cb> will be called whenever qemu or the guest
-writes anything to the console.
+In the current implementation, events are only generated
+synchronously: that means that events (and hence callbacks) can only
+happen while you are in the middle of making another libguestfs call.
+The callback is called in the same thread.

-Use this function to capture kernel messages and similar.
+Events may contain a payload, usually nothing (void), an array of 64
+bit unsigned integers, or a message buffer.  Payloads are discussed
+later on.

-Normally there is no log message handler, and log messages are just
-discarded.
+=head3 CLASSES OF EVENTS

-=head2 guestfs_set_subprocess_quit_callback
+=over 4

- typedef void (*guestfs_subprocess_quit_cb) (guestfs_h *g, void *opaque);
- void guestfs_set_subprocess_quit_callback (guestfs_h *g,
-                                            guestfs_subprocess_quit_cb cb,
-                                            void *opaque);
+=item GUESTFS_EVENT_CLOSE
+(payload type: void)

-The callback function C<cb> will be called when the child process
-quits, either asynchronously or if killed by
-L</guestfs_kill_subprocess>.  (This corresponds to a transition from
-any state to the CONFIG state).
+The callback function will be called while the handle is being closed
+(synchronously from L</guestfs_close>).

-=head2 guestfs_set_launch_done_callback
+Note that libguestfs installs an L<atexit(3)> handler to try to clean
+up handles that are open when the program exits.  This means that this
+callback might be called indirectly from L<exit(3)>, which can cause
+unexpected problems in higher-level languages (eg. if your HLL
+interpreter has already been cleaned up by the time this is called,
+and if your callback then jumps into some HLL function).

- typedef void (*guestfs_launch_done_cb) (guestfs_h *g, void *opaque);
- void guestfs_set_launch_done_callback (guestfs_h *g,
-                                        guestfs_launch_done_cb cb,
-                                        void *opaque);
+If no callback is registered: the handle is closed without any
+callback being invoked.

-The callback function C<cb> will be called when the child process
-becomes ready first time after it has been launched.  (This
-corresponds to a transition from LAUNCHING to the READY state).
+=item GUESTFS_EVENT_SUBPROCESS_QUIT
+(payload type: void)

-=head2 guestfs_set_close_callback
+The callback function will be called when the child process quits,
+either asynchronously or if killed by L</guestfs_kill_subprocess>.
+(This corresponds to a transition from any state to the CONFIG state).

- typedef void (*guestfs_close_cb) (guestfs_h *g, void *opaque);
- void guestfs_set_close_callback (guestfs_h *g,
-                                  guestfs_close_cb cb,
-                                  void *opaque);
+If no callback is registered: the event is ignored.

-The callback function C<cb> will be called while the handle
-is being closed (synchronously from L</guestfs_close>).
+=item GUESTFS_EVENT_LAUNCH_DONE
+(payload type: void)

-Note that libguestfs installs an L<atexit(3)> handler to try to
-clean up handles that are open when the program exits.  This
-means that this callback might be called indirectly from
-L<exit(3)>, which can cause unexpected problems in higher-level
-languages (eg. if your HLL interpreter has already been cleaned
-up by the time this is called, and if your callback then jumps
-into some HLL function).
+The callback function will be called when the child process becomes
+ready first time after it has been launched.  (This corresponds to a
+transition from LAUNCHING to the READY state).

-=head2 guestfs_set_progress_callback
+If no callback is registered: the event is ignored.

- typedef void (*guestfs_progress_cb) (guestfs_h *g, void *opaque,
-                                      int proc_nr, int serial,
-                                      uint64_t position, uint64_t total);
- void guestfs_set_progress_callback (guestfs_h *g,
-                                     guestfs_progress_cb cb,
-                                     void *opaque);
+=item GUESTFS_EVENT_PROGRESS
+(payload type: array of 4 x uint64_t)

 Some long-running operations can generate progress messages.  If
 this callback is registered, then it will be called each time a
@@ -1766,7 +1755,9 @@ progress message is generated (usually two seconds after
the
 operation started, and three times per second thereafter until
 it completes, although the frequency may change in future versions).

-The callback receives two numbers: C<position> and C<total>.
+The callback receives in the payload four unsigned 64 bit numbers
+which are (in order): C<proc_nr>, C<serial>, C<position>,
C<total>.
+
 The units of C<total> are not defined, although for some
 operations C<total> may relate in some way to the amount of
 data to be transferred (eg. in bytes or megabytes), and
@@ -1796,10 +1787,126 @@ requiring special code to detect this case.

 =back

-The callback also receives the procedure number and serial number of
-the call.  These are only useful for debugging protocol issues, and
-the callback can normally ignore them.  The callback may want to
-print these numbers in error messages or debugging messages.
+The callback also receives the procedure number (C<proc_nr>) and
+serial number (C<serial>) of the call.  These are only useful for
+debugging protocol issues, and the callback can normally ignore them.
+The callback may want to print these numbers in error messages or
+debugging messages.
+
+If no callback is registered: progress messages are discarded.
+
+=item GUESTFS_EVENT_APPLIANCE
+(payload type: message buffer)
+
+The callback function is called whenever a log message is generated by
+qemu, the appliance kernel, guestfsd (daemon), or utility programs.
+
+If the verbose flag (L</guestfs_set_verbose>) is set before launch
+(L</guestfs_launch>) then additional debug messages are generated.
+
+If no callback is registered: the messages are discarded unless the
+verbose flag is set in which case they are sent to stderr.  You can
+override the printing of verbose messages to stderr by setting up a
+callback.
+
+=item GUESTFS_EVENT_LIBRARY
+(payload type: message buffer)
+
+The callback function is called whenever a log message is generated by
+the library part of libguestfs.
+
+If the verbose flag (L</guestfs_set_verbose>) is set then additional
+debug messages are generated.
+
+If no callback is registered: the messages are discarded unless the
+verbose flag is set in which case they are sent to stderr.  You can
+override the printing of verbose messages to stderr by setting up a
+callback.
+
+=item GUESTFS_EVENT_TRACE
+(payload type: message buffer)
+
+The callback function is called whenever a trace message is generated.
+This only applies if the trace flag (L</guestfs_set_trace>) is set.
+
+If no callback is registered: the messages are sent to stderr.  You
+can override the printing of trace messages to stderr by setting up a
+callback.
+
+=back
+
+=head3 guestfs_set_event_callback
+
+ int guestfs_set_event_callback (guestfs_h *g,
+                                 guestfs_event_callback cb,
+                                 uint64_t event_bitmask,
+                                 int flags,
+                                 void *opaque);
+
+This function registers a callback (C<cb>) for all event classes
+in the C<event_bitmask>.
+
+For example, to register for all log message events, you could call
+this function with the bitmask
+C<GUESTFS_EVENT_APPLIANCE|GUESTFS_EVENT_LIBRARY>.  To register a
+single callback for all possible classes of events, use
+C<GUESTFS_EVENT_ALL>.
+
+C<flags> should always be passed as 0.
+
+C<opaque> is an opaque pointer which is passed to the callback.  You
+can use it for any purpose.
+
+The return value is the event handle (an integer) which you can use to
+delete the callback (see below).
+
+If there is an error, this function returns C<-1>, and sets the error
+in the handle in the usual way (see L</guestfs_last_error> etc.)
+
+Callbacks remain in effect until they are deleted, or until the handle
+is closed.
+
+In the case where multiple callbacks are registered for a particular
+event class, all of the callbacks are called.  The order in which
+multiple callbacks are called is not defined.
+
+=head3 guestfs_delete_event_callback
+
+ void guestfs_delete_event_callback (guestfs_h *g, int event_handle);
+
+Delete a callback that was previously registered.  C<event_handle>
+should be the integer that was returned by a previous call to
+C<guestfs_set_event_callback> on the same handle.
+
+=head3 guestfs_event_callback
+
+ typedef void (*guestfs_event_callback) (
+                  guestfs_h *g,
+                  void *opaque,
+                  uint64_t event,
+                  int event_handle,
+                  int flags,
+                  const char *buf, size_t buf_len,
+                  const uint64_t *array, size_t array_len);
+
+This is the type of the event callback function that you have to
+provide.
+
+The basic parameters are: the handle (C<g>), the opaque user pointer
+(C<opaque>), the event class (eg. C<GUESTFS_EVENT_PROGRESS>), the
+event handle, and C<flags> which in the current API you should ignore.
+
+The remaining parameters contain the event payload (if any).  Each
+event may contain a payload, which usually relates to the event class,
+but for future proofing your code should be written to handle any
+payload for any event class.
+
+C<buf> and C<buf_len> contain a message buffer (if C<buf_len ==
0>,
+then there is no message buffer).  Note that this message buffer can
+contain arbitrary 8 bit data, including NUL bytes.
+
+C<array> and C<array_len> is an array of 64 bit unsigned integers.
At
+the moment this is only used for progress messages.

 =head1 PRIVATE DATA AREA

@@ -1833,8 +1940,7 @@ any way.  As far as libguestfs is concerned, it need not
be a valid
 pointer at all.  In particular, libguestfs does I<not> try to free the
 data when the handle is closed.  If the data must be freed, then the
 caller must either free it before calling L</guestfs_close> or must
-set up a close callback to do it (see L</guestfs_set_close_callback>,
-and note that only one callback can be registered for a handle).
+set up a close callback to do it (see L</GUESTFS_EVENT_CLOSE>.

 The private data area is implemented using a hash table, and should be
 reasonably efficient for moderate numbers of keys.
@@ -2100,8 +2206,8 @@ are distinguished by the normal length word being replaced
by
 C<GUESTFS_PROGRESS_FLAG>, followed by a fixed size progress message.

 The library turns them into progress callbacks (see
-C<guestfs_set_progress_callback>) if there is a callback registered,
-or discards them if not.
+L</GUESTFS_EVENT_PROGRESS>) if there is a callback registered, or
+discards them if not.

 The daemon self-limits the frequency of progress messages it sends
 (see C<daemon/proto.c:notify_progress>).  Not all calls generate
diff --git a/src/inspect.c b/src/inspect.c
index 99097ee..7cf18c3 100644
--- a/src/inspect.c
+++ b/src/inspect.c
@@ -258,10 +258,9 @@ check_for_filesystem_on (guestfs_h *g, const char *device,

   int is_swap = vfs_type && STREQ (vfs_type, "swap");

-  if (g->verbose)
-    fprintf (stderr, "check_for_filesystem_on: %s %d %d (%s)\n",
-             device, is_block, is_partnum,
-             vfs_type ? vfs_type : "failed to get vfs type");
+  debug (g, "check_for_filesystem_on: %s %d %d (%s)",
+         device, is_block, is_partnum,
+         vfs_type ? vfs_type : "failed to get vfs type");

   if (is_swap) {
     free (vfs_type);
@@ -1307,8 +1306,7 @@ add_fstab_entry (guestfs_h *g, struct inspect_fs *fs,
   fs->fstab[n-1].device = device;
   fs->fstab[n-1].mountpoint = mountpoint;

-  if (g->verbose)
-    fprintf (stderr, "fstab: device=%s mountpoint=%s\n", device,
mountpoint);
+  debug (g, "fstab: device=%s mountpoint=%s", device, mountpoint);

   return 0;
 }
@@ -1414,8 +1412,7 @@ check_windows_root (guestfs_h *g, struct inspect_fs *fs)
     return -1;
   }

-  if (g->verbose)
-    fprintf (stderr, "windows %%SYSTEMROOT%% = %s", systemroot);
+  debug (g, "windows %%SYSTEMROOT%% = %s", systemroot);

   /* Freed by guestfs___free_inspect_info. */
   fs->windows_systemroot = systemroot;
@@ -2219,8 +2216,7 @@ list_applications_rpm (guestfs_h *g, struct inspect_fs
*fs)

   snprintf (cmd, cmd_len, DB_DUMP " -p '%s'", tmpfile);

-  if (g->verbose)
-    fprintf (stderr, "list_applications_rpm: %s\n", cmd);
+  debug (g, "list_applications_rpm: %s", cmd);

   pp = popen (cmd, "r");
   if (pp == NULL) {
@@ -2940,7 +2936,7 @@ guestfs___match (guestfs_h *g, const char *str, const pcre
*re)
     return 0;
   if (r != 1) {
     /* Internal error -- should not happen. */
-    fprintf (stderr, "libguestfs: %s: %s: internal error: pcre_exec
returned unexpected error code %d when matching against the string
\"%s\"\n",
+    warning (g, "%s: %s: pcre_exec returned unexpected error code %d when
matching against the string \"%s\"\n",
              __FILE__, __func__, r, str);
     return 0;
   }
@@ -2963,7 +2959,7 @@ guestfs___match1 (guestfs_h *g, const char *str, const
pcre *re)
     return NULL;
   if (r != 2) {
     /* Internal error -- should not happen. */
-    fprintf (stderr, "libguestfs: %s: %s: internal error: pcre_exec
returned unexpected error code %d when matching against the string
\"%s\"\n",
+    warning (g, "%s: %s: internal error: pcre_exec returned unexpected
error code %d when matching against the string \"%s\"",
              __FILE__, __func__, r, str);
     return NULL;
   }
@@ -2984,7 +2980,7 @@ guestfs___match2 (guestfs_h *g, const char *str, const
pcre *re,
     return 0;
   if (r != 3) {
     /* Internal error -- should not happen. */
-    fprintf (stderr, "libguestfs: %s: %s: internal error: pcre_exec
returned unexpected error code %d when matching against the string
\"%s\"\n",
+    warning (g, "%s: %s: internal error: pcre_exec returned unexpected
error code %d when matching against the string \"%s\"",
              __FILE__, __func__, r, str);
     return 0;
   }
@@ -3008,7 +3004,7 @@ guestfs___match3 (guestfs_h *g, const char *str, const
pcre *re,
     return 0;
   if (r != 4) {
     /* Internal error -- should not happen. */
-    fprintf (stderr, "libguestfs: %s: %s: internal error: pcre_exec
returned unexpected error code %d when matching against the string
\"%s\"\n",
+    warning (g, "%s: %s: internal error: pcre_exec returned unexpected
error code %d when matching against the string \"%s\"",
              __FILE__, __func__, r, str);
     return 0;
   }
diff --git a/src/launch.c b/src/launch.c
index e50985d..d2ed948 100644
--- a/src/launch.c
+++ b/src/launch.c
@@ -373,7 +373,7 @@ guestfs__launch (guestfs_h *g)
    * want. (RHBZ#610880).
    */
   if (chmod (g->tmpdir, 0755) == -1)
-    fprintf (stderr, "chmod: %s: %m (ignored)\n", g->tmpdir);
+    warning (g, "chmod: %s: %m (ignored)", g->tmpdir);

   /* Launch the appliance or attach to an existing daemon. */
   switch (g->attach_method) {
@@ -918,27 +918,40 @@ guestfs___print_timestamped_argv (guestfs_h *g, const char
* argv[])
 {
   int i = 0;
   int needs_quote;
+  char *buf = NULL;
+  size_t len;
+  FILE *fp;
+
+  fp = open_memstream (&buf, &len);
+  if (fp == NULL) {
+    warning (g, "open_memstream: %m");
+    return;
+  }

   struct timeval tv;
   gettimeofday (&tv, NULL);
-  fprintf (stderr, "[%05" PRIi64 "ms] ", timeval_diff
(&g->launch_t, &tv));
+  fprintf (fp, "[%05" PRIi64 "ms] ", timeval_diff
(&g->launch_t, &tv));

   while (argv[i]) {
     if (argv[i][0] == '-') /* -option starts a new line */
-      fprintf (stderr, " \\\n   ");
+      fprintf (fp, " \\\n   ");

-    if (i > 0) fputc (' ', stderr);
+    if (i > 0) fputc (' ', fp);

     /* Does it need shell quoting?  This only deals with simple cases. */
     needs_quote = strcspn (argv[i], " ") != strlen (argv[i]);

-    if (needs_quote) fputc ('\'', stderr);
-    fprintf (stderr, "%s", argv[i]);
-    if (needs_quote) fputc ('\'', stderr);
+    if (needs_quote) fputc ('\'', fp);
+    fprintf (fp, "%s", argv[i]);
+    if (needs_quote) fputc ('\'', fp);
     i++;
   }

-  fputc ('\n', stderr);
+  fclose (fp);
+
+  debug (g, "%s", buf);
+
+  free (buf);
 }

 void
@@ -957,8 +970,7 @@ guestfs___print_timestamped_message (guestfs_h *g, const
char *fs, ...)

   gettimeofday (&tv, NULL);

-  fprintf (stderr, "[%05" PRIi64 "ms] %s\n",
-           timeval_diff (&g->launch_t, &tv), msg);
+  debug (g, "[%05" PRIi64 "ms] %s", timeval_diff
(&g->launch_t, &tv), msg);

   free (msg);
 }
@@ -1064,8 +1076,7 @@ is_openable (guestfs_h *g, const char *path, int flags)
 {
   int fd = open (path, flags);
   if (fd == -1) {
-    if (g->verbose)
-      perror (path);
+    debug (g, "is_openable: %s: %m", path);
     return 0;
   }
   close (fd);
@@ -1094,8 +1105,7 @@ guestfs__kill_subprocess (guestfs_h *g)
     return -1;
   }

-  if (g->verbose)
-    fprintf (stderr, "sending SIGTERM to process %d\n", g->pid);
+  debug (g, "sending SIGTERM to process %d", g->pid);

   if (g->pid > 0) kill (g->pid, SIGTERM);
   if (g->recoverypid > 0) kill (g->recoverypid, 9);
diff --git a/src/proto.c b/src/proto.c
index 549734b..fb582cf 100644
--- a/src/proto.c
+++ b/src/proto.c
@@ -176,8 +176,7 @@ guestfs___end_busy (guestfs_h *g)
 static void
 child_cleanup (guestfs_h *g)
 {
-  if (g->verbose)
-    fprintf (stderr, "child_cleanup: %p: child process died\n", g);
+  debug (g, "child_cleanup: %p: child process died", g);

   /*if (g->pid > 0) kill (g->pid, SIGTERM);*/
   if (g->recoverypid > 0) kill (g->recoverypid, 9);
@@ -193,8 +192,7 @@ child_cleanup (guestfs_h *g)
   g->recoverypid = 0;
   memset (&g->launch_t, 0, sizeof g->launch_t);
   g->state = CONFIG;
-  if (g->subprocess_quit_cb)
-    g->subprocess_quit_cb (g, g->subprocess_quit_cb_data);
+  guestfs___call_callbacks_void (g, GUESTFS_EVENT_SUBPROCESS_QUIT);
 }

 static int
@@ -204,10 +202,8 @@ read_log_message_or_eof (guestfs_h *g, int fd, int
error_if_eof)
   int n;

 #if 0
-  if (g->verbose)
-    fprintf (stderr,
-             "read_log_message_or_eof: %p g->state = %d, fd =
%d\n",
-             g, g->state, fd);
+  debug (g, "read_log_message_or_eof: %p g->state = %d, fd = %d",
+         g, g->state, fd);
 #endif

   /* QEMU's console emulates a 16550A serial port.  The real 16550A
@@ -237,13 +233,8 @@ read_log_message_or_eof (guestfs_h *g, int fd, int
error_if_eof)
     return -1;
   }

-  /* In verbose mode, copy all log messages to stderr. */
-  if (g->verbose)
-    ignore_value (write (STDERR_FILENO, buf, n));
-
   /* It's an actual log message, send it upwards if anyone is listening. */
-  if (g->log_message_cb)
-    g->log_message_cb (g, g->log_message_cb_data, buf, n);
+  guestfs___call_callbacks_message (g, GUESTFS_EVENT_APPLIANCE, buf, n);

   return 0;
 }
@@ -293,6 +284,20 @@ really_read_from_socket (guestfs_h *g, int sock, char *buf,
size_t n)
   return (ssize_t) got;
 }

+static void
+send_progress_message (guestfs_h *g, const guestfs_progress *message)
+{
+  uint64_t array[4];
+
+  array[0] = message->proc;
+  array[1] = message->serial;
+  array[2] = message->position;
+  array[3] = message->total;
+
+  guestfs___call_callbacks_array (g, GUESTFS_EVENT_PROGRESS,
+                                  array, sizeof array / sizeof array[0]);
+}
+
 static int
 check_for_daemon_cancellation_or_eof (guestfs_h *g, int fd)
 {
@@ -301,10 +306,8 @@ check_for_daemon_cancellation_or_eof (guestfs_h *g, int fd)
   uint32_t flag;
   XDR xdr;

-  if (g->verbose)
-    fprintf (stderr,
-             "check_for_daemon_cancellation_or_eof: %p g->state = %d,
fd = %d\n",
-             g, g->state, fd);
+  debug (g, "check_for_daemon_cancellation_or_eof: %p g->state = %d, fd
= %d",
+         g, g->state, fd);

   n = really_read_from_socket (g, fd, buf, 4);
   if (n == -1)
@@ -331,16 +334,14 @@ check_for_daemon_cancellation_or_eof (guestfs_h *g, int
fd)
       return -1;
     }

-    if (g->state == BUSY && g->progress_cb) {
+    if (g->state == BUSY) {
       guestfs_progress message;

       xdrmem_create (&xdr, buf, PROGRESS_MESSAGE_SIZE, XDR_DECODE);
       xdr_guestfs_progress (&xdr, &message);
       xdr_destroy (&xdr);

-      g->progress_cb (g, g->progress_cb_data,
-                      message.proc, message.serial,
-                      message.position, message.total);
+      send_progress_message (g, &message);
     }

     return 0;
@@ -374,9 +375,7 @@ guestfs___send_to_daemon (guestfs_h *g, const void *v_buf,
size_t n)
   fd_set rset, rset2;
   fd_set wset, wset2;

-  if (g->verbose)
-    fprintf (stderr,
-             "send_to_daemon: %p g->state = %d, n = %zu\n", g,
g->state, n);
+  debug (g, "send_to_daemon: %p g->state = %d, n = %zu", g,
g->state, n);

   FD_ZERO (&rset);
   FD_ZERO (&wset);
@@ -454,10 +453,8 @@ guestfs___recv_from_daemon (guestfs_h *g, uint32_t
*size_rtn, void **buf_rtn)
 {
   fd_set rset, rset2;

-  if (g->verbose)
-    fprintf (stderr,
-             "recv_from_daemon: %p g->state = %d, size_rtn = %p,
buf_rtn = %p\n",
-             g, g->state, size_rtn, buf_rtn);
+  debug (g, "recv_from_daemon: %p g->state = %d, size_rtn = %p, buf_rtn
= %p",
+         g, g->state, size_rtn, buf_rtn);

   FD_ZERO (&rset);

@@ -543,8 +540,7 @@ guestfs___recv_from_daemon (guestfs_h *g, uint32_t
*size_rtn, void **buf_rtn)
                    g->state);
           else {
             g->state = READY;
-            if (g->launch_done_cb)
-              g->launch_done_cb (g, g->launch_done_cb_data);
+            guestfs___call_callbacks_void (g, GUESTFS_EVENT_LAUNCH_DONE);
           }
           return 0;
         }
@@ -614,16 +610,14 @@ guestfs___recv_from_daemon (guestfs_h *g, uint32_t
*size_rtn, void **buf_rtn)
 #endif

   if (*size_rtn == GUESTFS_PROGRESS_FLAG) {
-    if (g->state == BUSY && g->progress_cb) {
+    if (g->state == BUSY) {
       guestfs_progress message;
       XDR xdr;
       xdrmem_create (&xdr, *buf_rtn, PROGRESS_MESSAGE_SIZE, XDR_DECODE);
       xdr_guestfs_progress (&xdr, &message);
       xdr_destroy (&xdr);

-      g->progress_cb (g, g->progress_cb_data,
-                      message.proc, message.serial,
-                      message.position, message.total);
+      send_progress_message (g, &message);
     }

     free (*buf_rtn);
@@ -646,9 +640,7 @@ guestfs___accept_from_daemon (guestfs_h *g)
 {
   fd_set rset, rset2;

-  if (g->verbose)
-    fprintf (stderr,
-             "accept_from_daemon: %p g->state = %d\n", g,
g->state);
+  debug (g, "accept_from_daemon: %p g->state = %d", g,
g->state);

   FD_ZERO (&rset);

@@ -908,8 +900,7 @@ send_file_chunk (guestfs_h *g, int cancel, const char *buf,
size_t buflen)

   /* Did the daemon send a cancellation message? */
   if (r == -2) {
-    if (g->verbose)
-      fprintf (stderr, "got daemon cancellation\n");
+    debug (g, "got daemon cancellation");
     return -2;
   }

@@ -1030,9 +1021,8 @@ guestfs___recv_file (guestfs_h *g, const char *filename)
   char fbuf[4];
   uint32_t flag = GUESTFS_CANCEL_FLAG;

-  if (g->verbose)
-    fprintf (stderr, "%s: waiting for daemon to acknowledge
cancellation\n",
-             __func__);
+  debug (g, "%s: waiting for daemon to acknowledge cancellation",
+         __func__);

   xdrmem_create (&xdr, fbuf, sizeof fbuf, XDR_ENCODE);
   xdr_uint32_t (&xdr, &flag);
--
1.7.4