Perl bindings fix: Not enough memory was allocated for array params.
[libguestfs.git] / src / generator.ml
index 2535120..7f24802 100755 (executable)
@@ -33,6 +33,7 @@
  *)
 
 #load "unix.cma";;
+#load "str.cma";;
 
 open Printf
 
@@ -135,7 +136,15 @@ can easily destroy all your data>."
  * the virtual machine and block devices are reused between tests.
  * So don't try testing kill_subprocess :-x
  *
- * Between each test we umount-all and lvm-remove-all (except InitNone).
+ * Between each test we blockdev-setrw, umount-all, lvm-remove-all
+ * (except InitNone).
+ *
+ * If the appliance is running an older Linux kernel (eg. RHEL 5) then
+ * devices are named /dev/hda etc.  To cope with this, the test suite
+ * adds some hairly logic to detect this case, and then automagically
+ * replaces all strings which match "/dev/sd.*" with "/dev/hd.*".
+ * When writing test cases you shouldn't have to worry about this
+ * difference.
  *
  * Don't assume anything about the previous contents of the block
  * devices.  Use 'Init*' to create some initial scenarios.
@@ -284,6 +293,32 @@ The first character of C<param> string must be a C<-> (dash).
 
 C<value> can be NULL.");
 
+  ("set_qemu", (RErr, [String "qemu"]), -1, [FishAlias "qemu"],
+   [],
+   "set the qemu binary",
+   "\
+Set the qemu binary that we will use.
+
+The default is chosen when the library was compiled by the
+configure script.
+
+You can also override this by setting the C<LIBGUESTFS_QEMU>
+environment variable.
+
+The string C<qemu> is stashed in the libguestfs handle, so the caller
+must make sure it remains valid for the lifetime of the handle.
+
+Setting C<qemu> to C<NULL> restores the default qemu binary.");
+
+  ("get_qemu", (RConstString "qemu", []), -1, [],
+   [],
+   "get the qemu binary",
+   "\
+Return the current qemu binary.
+
+This is always non-NULL.  If it wasn't set already, then this will
+return the default qemu binary name.");
+
   ("set_path", (RErr, [String "path"]), -1, [FishAlias "path"],
    [],
    "set the search path",
@@ -312,8 +347,12 @@ return the default path.");
    "set autosync mode",
    "\
 If C<autosync> is true, this enables autosync.  Libguestfs will make a
-best effort attempt to run C<guestfs_sync> when the handle is closed
-(also if the program exits without closing handles).");
+best effort attempt to run C<guestfs_umount_all> followed by
+C<guestfs_sync> when the handle is closed
+(also if the program exits without closing handles).
+
+This is disabled by default (except in guestfish where it is
+enabled by default).");
 
   ("get_autosync", (RBool "autosync", []), -1, [],
    [],
@@ -380,6 +419,35 @@ This returns the current state as an opaque integer.  This is
 only useful for printing debug and internal error messages.
 
 For more information on states, see L<guestfs(3)>.");
+
+  ("set_busy", (RErr, []), -1, [NotInFish],
+   [],
+   "set state to busy",
+   "\
+This sets the state to C<BUSY>.  This is only used when implementing
+actions using the low-level API.
+
+For more information on states, see L<guestfs(3)>.");
+
+  ("set_ready", (RErr, []), -1, [NotInFish],
+   [],
+   "set state to ready",
+   "\
+This sets the state to C<READY>.  This is only used when implementing
+actions using the low-level API.
+
+For more information on states, see L<guestfs(3)>.");
+
+  ("end_busy", (RErr, []), -1, [NotInFish],
+   [],
+   "leave the busy state",
+   "\
+This sets the state to C<READY>, or if in C<CONFIG> then it leaves the
+state as is.  This is only used when implementing
+actions using the low-level API.
+
+For more information on states, see L<guestfs(3)>.");
+
 ]
 
 let daemon_functions = [
@@ -939,7 +1007,7 @@ on the volume group C<volgroup>, with C<size> megabytes.");
    "make a filesystem",
    "\
 This creates a filesystem on C<device> (usually a partition
-of LVM logical volume).  The filesystem type is C<fstype>, for
+or LVM logical volume).  The filesystem type is C<fstype>, for
 example C<ext3>.");
 
   ("sfdisk", (RErr, [String "device";
@@ -995,7 +1063,12 @@ with length C<size>.
 
 As a special case, if C<size> is C<0>
 then the length is calculated using C<strlen> (so in this case
-the content cannot contain embedded ASCII NULs).");
+the content cannot contain embedded ASCII NULs).
+
+I<NB.> Owing to a bug, writing content containing ASCII NUL
+characters does I<not> work, even if the length is specified.
+We hope to resolve this bug in a future version.  In the meantime
+use C<guestfs_upload>.");
 
   ("umount", (RErr, [String "pathordevice"]), 45, [FishAlias "unmount"],
    [InitEmpty, TestOutputList (
@@ -1028,6 +1101,20 @@ Some internal mounts are not shown.");
   ("umount_all", (RErr, []), 47, [FishAlias "unmount-all"],
    [InitBasicFS, TestOutputList (
       [["umount_all"];
+       ["mounts"]], []);
+    (* check that umount_all can unmount nested mounts correctly: *)
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mkfs"; "ext2"; "/dev/sda2"];
+       ["mkfs"; "ext2"; "/dev/sda3"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["mkdir"; "/mp1"];
+       ["mount"; "/dev/sda2"; "/mp1"];
+       ["mkdir"; "/mp1/mp2"];
+       ["mount"; "/dev/sda3"; "/mp1/mp2"];
+       ["mkdir"; "/mp1/mp2/mp3"];
+       ["umount_all"];
        ["mounts"]], [])],
    "unmount all filesystems",
    "\
@@ -1132,10 +1219,10 @@ This is the same as the C<statvfs(2)> system call.");
 
   ("tune2fs_l", (RHashtable "superblock", [String "device"]), 55, [],
    [], (* XXX test *)
-   "get ext2/ext3 superblock details",
+   "get ext2/ext3/ext4 superblock details",
    "\
-This returns the contents of the ext2 or ext3 filesystem superblock
-on C<device>.
+This returns the contents of the ext2, ext3 or ext4 filesystem
+superblock on C<device>.
 
 It is the same as running C<tune2fs -l device>.  See L<tune2fs(8)>
 manpage for more details.  The list of fields returned isn't
@@ -1253,9 +1340,11 @@ Reread the partition table on C<device>.
 
 This uses the L<blockdev(8)> command.");
 
-(*
   ("upload", (RErr, [FileIn "filename"; String "remotefilename"]), 66, [],
-   [],
+   [InitBasicFS, TestOutput (
+      (* Pick a file from cwd which isn't likely to change. *)
+    [["upload"; "COPYING.LIB"; "/COPYING.LIB"];
+     ["checksum"; "md5"; "/COPYING.LIB"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
    "upload a file from the local machine",
    "\
 Upload local file C<filename> to C<remotefilename> on the
@@ -1266,7 +1355,12 @@ C<filename> can also be a named pipe.
 See also C<guestfs_download>.");
 
   ("download", (RErr, [String "remotefilename"; FileOut "filename"]), 67, [],
-   [],
+   [InitBasicFS, TestOutput (
+      (* Pick a file from cwd which isn't likely to change. *)
+    [["upload"; "COPYING.LIB"; "/COPYING.LIB"];
+     ["download"; "/COPYING.LIB"; "testdownload.tmp"];
+     ["upload"; "testdownload.tmp"; "/upload"];
+     ["checksum"; "md5"; "/upload"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
    "download a file to the local machine",
    "\
 Download file C<remotefilename> and save it as C<filename>
@@ -1275,7 +1369,490 @@ on the local machine.
 C<filename> can also be a named pipe.
 
 See also C<guestfs_upload>, C<guestfs_cat>.");
-*)
+
+  ("checksum", (RString "checksum", [String "csumtype"; String "path"]), 68, [],
+   [InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "crc"; "/new"]], "935282863");
+    InitBasicFS, TestLastFail (
+      [["checksum"; "crc"; "/new"]]);
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "md5"; "/new"]], "d8e8fca2dc0f896fd7cb4cb0031ba249");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha1"; "/new"]], "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha224"; "/new"]], "52f1bf093f4b7588726035c176c0cdb4376cfea53819f1395ac9e6ec");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha256"; "/new"]], "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha384"; "/new"]], "109bb6b5b6d5547c1ce03c7a8bd7d8f80c1cb0957f50c4f7fda04692079917e4f9cad52b878f3d8234e1a170b154b72d");
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha512"; "/new"]], "0e3e75234abc68f4378a86b3f4b32a198ba301845b0cd6e50106e874345700cc6663a86c1ea125dc5e92be17c98f9a0f85ca9d5f595db2012f7cc3571945c123")],
+   "compute MD5, SHAx or CRC checksum of file",
+   "\
+This call computes the MD5, SHAx or CRC checksum of the
+file named C<path>.
+
+The type of checksum to compute is given by the C<csumtype>
+parameter which must have one of the following values:
+
+=over 4
+
+=item C<crc>
+
+Compute the cyclic redundancy check (CRC) specified by POSIX
+for the C<cksum> command.
+
+=item C<md5>
+
+Compute the MD5 hash (using the C<md5sum> program).
+
+=item C<sha1>
+
+Compute the SHA1 hash (using the C<sha1sum> program).
+
+=item C<sha224>
+
+Compute the SHA224 hash (using the C<sha224sum> program).
+
+=item C<sha256>
+
+Compute the SHA256 hash (using the C<sha256sum> program).
+
+=item C<sha384>
+
+Compute the SHA384 hash (using the C<sha384sum> program).
+
+=item C<sha512>
+
+Compute the SHA512 hash (using the C<sha512sum> program).
+
+=back
+
+The checksum is returned as a printable string.");
+
+  ("tar_in", (RErr, [FileIn "tarfile"; String "directory"]), 69, [],
+   [InitBasicFS, TestOutput (
+      [["tar_in"; "images/helloworld.tar"; "/"];
+       ["cat"; "/hello"]], "hello\n")],
+   "unpack tarfile to directory",
+   "\
+This command uploads and unpacks local file C<tarfile> (an
+I<uncompressed> tar file) into C<directory>.
+
+To upload a compressed tarball, use C<guestfs_tgz_in>.");
+
+  ("tar_out", (RErr, [String "directory"; FileOut "tarfile"]), 70, [],
+   [],
+   "pack directory into tarfile",
+   "\
+This command packs the contents of C<directory> and downloads
+it to local file C<tarfile>.
+
+To download a compressed tarball, use C<guestfs_tgz_out>.");
+
+  ("tgz_in", (RErr, [FileIn "tarball"; String "directory"]), 71, [],
+   [InitBasicFS, TestOutput (
+      [["tgz_in"; "images/helloworld.tar.gz"; "/"];
+       ["cat"; "/hello"]], "hello\n")],
+   "unpack compressed tarball to directory",
+   "\
+This command uploads and unpacks local file C<tarball> (a
+I<gzip compressed> tar file) into C<directory>.
+
+To upload an uncompressed tarball, use C<guestfs_tar_in>.");
+
+  ("tgz_out", (RErr, [String "directory"; FileOut "tarball"]), 72, [],
+   [],
+   "pack directory into compressed tarball",
+   "\
+This command packs the contents of C<directory> and downloads
+it to local file C<tarball>.
+
+To download an uncompressed tarball, use C<guestfs_tar_out>.");
+
+  ("mount_ro", (RErr, [String "device"; String "mountpoint"]), 73, [],
+   [InitBasicFS, TestLastFail (
+      [["umount"; "/"];
+       ["mount_ro"; "/dev/sda1"; "/"];
+       ["touch"; "/new"]]);
+    InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "data"; "0"];
+       ["umount"; "/"];
+       ["mount_ro"; "/dev/sda1"; "/"];
+       ["cat"; "/new"]], "data")],
+   "mount a guest disk, read-only",
+   "\
+This is the same as the C<guestfs_mount> command, but it
+mounts the filesystem with the read-only (I<-o ro>) flag.");
+
+  ("mount_options", (RErr, [String "options"; String "device"; String "mountpoint"]), 74, [],
+   [],
+   "mount a guest disk with mount options",
+   "\
+This is the same as the C<guestfs_mount> command, but it
+allows you to set the mount options as for the
+L<mount(8)> I<-o> flag.");
+
+  ("mount_vfs", (RErr, [String "options"; String "vfstype"; String "device"; String "mountpoint"]), 75, [],
+   [],
+   "mount a guest disk with mount options and vfstype",
+   "\
+This is the same as the C<guestfs_mount> command, but it
+allows you to set both the mount options and the vfstype
+as for the L<mount(8)> I<-o> and I<-t> flags.");
+
+  ("debug", (RString "result", [String "subcmd"; StringList "extraargs"]), 76, [],
+   [],
+   "debugging and internals",
+   "\
+The C<guestfs_debug> command exposes some internals of
+C<guestfsd> (the guestfs daemon) that runs inside the
+qemu subprocess.
+
+There is no comprehensive help for this command.  You have
+to look at the file C<daemon/debug.c> in the libguestfs source
+to find out what you can do.");
+
+  ("lvremove", (RErr, [String "device"]), 77, [],
+   [InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["lvremove"; "/dev/VG/LV1"];
+       ["lvs"]], ["/dev/VG/LV2"]);
+    InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["lvremove"; "/dev/VG"];
+       ["lvs"]], []);
+    InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["lvremove"; "/dev/VG"];
+       ["vgs"]], ["VG"])],
+   "remove an LVM logical volume",
+   "\
+Remove an LVM logical volume C<device>, where C<device> is
+the path to the LV, such as C</dev/VG/LV>.
+
+You can also remove all LVs in a volume group by specifying
+the VG name, C</dev/VG>.");
+
+  ("vgremove", (RErr, [String "vgname"]), 78, [],
+   [InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["lvs"]], []);
+    InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["vgs"]], [])],
+   "remove an LVM volume group",
+   "\
+Remove an LVM volume group C<vgname>, (for example C<VG>).
+
+This also forcibly removes all logical volumes in the volume
+group (if any).");
+
+  ("pvremove", (RErr, [String "device"]), 79, [],
+   [InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["pvremove"; "/dev/sda"];
+       ["lvs"]], []);
+    InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["pvremove"; "/dev/sda"];
+       ["vgs"]], []);
+    InitEmpty, TestOutputList (
+      [["pvcreate"; "/dev/sda"];
+       ["vgcreate"; "VG"; "/dev/sda"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["pvremove"; "/dev/sda"];
+       ["pvs"]], [])],
+   "remove an LVM physical volume",
+   "\
+This wipes a physical volume C<device> so that LVM will no longer
+recognise it.
+
+The implementation uses the C<pvremove> command which refuses to
+wipe physical volumes that contain any volume groups, so you have
+to remove those first.");
+
+  ("set_e2label", (RErr, [String "device"; String "label"]), 80, [],
+   [InitBasicFS, TestOutput (
+      [["set_e2label"; "/dev/sda1"; "testlabel"];
+       ["get_e2label"; "/dev/sda1"]], "testlabel")],
+   "set the ext2/3/4 filesystem label",
+   "\
+This sets the ext2/3/4 filesystem label of the filesystem on
+C<device> to C<label>.  Filesystem labels are limited to
+16 characters.
+
+You can use either C<guestfs_tune2fs_l> or C<guestfs_get_e2label>
+to return the existing label on a filesystem.");
+
+  ("get_e2label", (RString "label", [String "device"]), 81, [],
+   [],
+   "get the ext2/3/4 filesystem label",
+   "\
+This returns the ext2/3/4 filesystem label of the filesystem on
+C<device>.");
+
+  ("set_e2uuid", (RErr, [String "device"; String "uuid"]), 82, [],
+   [InitBasicFS, TestOutput (
+      [["set_e2uuid"; "/dev/sda1"; "a3a61220-882b-4f61-89f4-cf24dcc7297d"];
+       ["get_e2uuid"; "/dev/sda1"]], "a3a61220-882b-4f61-89f4-cf24dcc7297d");
+    InitBasicFS, TestOutput (
+      [["set_e2uuid"; "/dev/sda1"; "clear"];
+       ["get_e2uuid"; "/dev/sda1"]], "");
+    (* We can't predict what UUIDs will be, so just check the commands run. *)
+    InitBasicFS, TestRun (
+      [["set_e2uuid"; "/dev/sda1"; "random"]]);
+    InitBasicFS, TestRun (
+      [["set_e2uuid"; "/dev/sda1"; "time"]])],
+   "set the ext2/3/4 filesystem UUID",
+   "\
+This sets the ext2/3/4 filesystem UUID of the filesystem on
+C<device> to C<uuid>.  The format of the UUID and alternatives
+such as C<clear>, C<random> and C<time> are described in the
+L<tune2fs(8)> manpage.
+
+You can use either C<guestfs_tune2fs_l> or C<guestfs_get_e2uuid>
+to return the existing UUID of a filesystem.");
+
+  ("get_e2uuid", (RString "uuid", [String "device"]), 83, [],
+   [],
+   "get the ext2/3/4 filesystem UUID",
+   "\
+This returns the ext2/3/4 filesystem UUID of the filesystem on
+C<device>.");
+
+  ("fsck", (RInt "status", [String "fstype"; String "device"]), 84, [],
+   [InitBasicFS, TestOutputInt (
+      [["umount"; "/dev/sda1"];
+       ["fsck"; "ext2"; "/dev/sda1"]], 0);
+    InitBasicFS, TestOutputInt (
+      [["umount"; "/dev/sda1"];
+       ["zero"; "/dev/sda1"];
+       ["fsck"; "ext2"; "/dev/sda1"]], 8)],
+   "run the filesystem checker",
+   "\
+This runs the filesystem checker (fsck) on C<device> which
+should have filesystem type C<fstype>.
+
+The returned integer is the status.  See L<fsck(8)> for the
+list of status codes from C<fsck>.
+
+Notes:
+
+=over 4
+
+=item *
+
+Multiple status codes can be summed together.
+
+=item *
+
+A non-zero return code can mean \"success\", for example if
+errors have been corrected on the filesystem.
+
+=item *
+
+Checking or repairing NTFS volumes is not supported
+(by linux-ntfs).
+
+=back
+
+This command is entirely equivalent to running C<fsck -a -t fstype device>.");
+
+  ("zero", (RErr, [String "device"]), 85, [],
+   [InitBasicFS, TestOutput (
+      [["umount"; "/dev/sda1"];
+       ["zero"; "/dev/sda1"];
+       ["file"; "/dev/sda1"]], "data")],
+   "write zeroes to the device",
+   "\
+This command writes zeroes over the first few blocks of C<device>.
+
+How many blocks are zeroed isn't specified (but it's I<not> enough
+to securely wipe the device).  It should be sufficient to remove
+any partition tables, filesystem superblocks and so on.");
+
+  ("grub_install", (RErr, [String "root"; String "device"]), 86, [],
+   [InitBasicFS, TestOutputTrue (
+      [["grub_install"; "/"; "/dev/sda1"];
+       ["is_dir"; "/boot"]])],
+   "install GRUB",
+   "\
+This command installs GRUB (the Grand Unified Bootloader) on
+C<device>, with the root directory being C<root>.");
+
+  ("cp", (RErr, [String "src"; String "dest"]), 87, [],
+   [InitBasicFS, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["cp"; "/old"; "/new"];
+       ["cat"; "/new"]], "file content");
+    InitBasicFS, TestOutputTrue (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["cp"; "/old"; "/new"];
+       ["is_file"; "/old"]]);
+    InitBasicFS, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mkdir"; "/dir"];
+       ["cp"; "/old"; "/dir/new"];
+       ["cat"; "/dir/new"]], "file content")],
+   "copy a file",
+   "\
+This copies a file from C<src> to C<dest> where C<dest> is
+either a destination filename or destination directory.");
+
+  ("cp_a", (RErr, [String "src"; String "dest"]), 88, [],
+   [InitBasicFS, TestOutput (
+      [["mkdir"; "/olddir"];
+       ["mkdir"; "/newdir"];
+       ["write_file"; "/olddir/file"; "file content"; "0"];
+       ["cp_a"; "/olddir"; "/newdir"];
+       ["cat"; "/newdir/olddir/file"]], "file content")],
+   "copy a file or directory recursively",
+   "\
+This copies a file or directory from C<src> to C<dest>
+recursively using the C<cp -a> command.");
+
+  ("mv", (RErr, [String "src"; String "dest"]), 89, [],
+   [InitBasicFS, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mv"; "/old"; "/new"];
+       ["cat"; "/new"]], "file content");
+    InitBasicFS, TestOutputFalse (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mv"; "/old"; "/new"];
+       ["is_file"; "/old"]])],
+   "move a file",
+   "\
+This moves a file from C<src> to C<dest> where C<dest> is
+either a destination filename or destination directory.");
+
+  ("drop_caches", (RErr, [Int "whattodrop"]), 90, [],
+   [InitEmpty, TestRun (
+      [["drop_caches"; "3"]])],
+   "drop kernel page cache, dentries and inodes",
+   "\
+This instructs the guest kernel to drop its page cache,
+and/or dentries and inode caches.  The parameter C<whattodrop>
+tells the kernel what precisely to drop, see
+L<http://linux-mm.org/Drop_Caches>
+
+Setting C<whattodrop> to 3 should drop everything.
+
+This automatically calls L<sync(2)> before the operation,
+so that the maximum guest memory is freed.");
+
+  ("dmesg", (RString "kmsgs", []), 91, [],
+   [InitEmpty, TestRun (
+      [["dmesg"]])],
+   "return kernel messages",
+   "\
+This returns the kernel messages (C<dmesg> output) from
+the guest kernel.  This is sometimes useful for extended
+debugging of problems.
+
+Another way to get the same information is to enable
+verbose messages with C<guestfs_set_verbose> or by setting
+the environment variable C<LIBGUESTFS_DEBUG=1> before
+running the program.");
+
+  ("ping_daemon", (RErr, []), 92, [],
+   [InitEmpty, TestRun (
+      [["ping_daemon"]])],
+   "ping the guest daemon",
+   "\
+This is a test probe into the guestfs daemon running inside
+the qemu subprocess.  Calling this function checks that the
+daemon responds to the ping message, without affecting the daemon
+or attached block device(s) in any other way.");
+
+  ("equal", (RBool "equality", [String "file1"; String "file2"]), 93, [],
+   [InitBasicFS, TestOutputTrue (
+      [["write_file"; "/file1"; "contents of a file"; "0"];
+       ["cp"; "/file1"; "/file2"];
+       ["equal"; "/file1"; "/file2"]]);
+    InitBasicFS, TestOutputFalse (
+      [["write_file"; "/file1"; "contents of a file"; "0"];
+       ["write_file"; "/file2"; "contents of another file"; "0"];
+       ["equal"; "/file1"; "/file2"]]);
+    InitBasicFS, TestLastFail (
+      [["equal"; "/file1"; "/file2"]])],
+   "test if two files have equal contents",
+   "\
+This compares the two files C<file1> and C<file2> and returns
+true if their content is exactly equal, or false otherwise.
+
+The external L<cmp(1)> program is used for the comparison.");
+
+  ("strings", (RStringList "stringsout", [String "path"]), 94, [ProtocolLimitWarning],
+   [InitBasicFS, TestOutputList (
+      [["write_file"; "/new"; "hello\nworld\n"; "0"];
+       ["strings"; "/new"]], ["hello"; "world"])],
+   "print the printable strings in a file",
+   "\
+This runs the L<strings(1)> command on a file and returns
+the list of printable strings found.");
+
+  ("strings_e", (RStringList "stringsout", [String "encoding"; String "path"]), 95, [ProtocolLimitWarning],
+   [InitBasicFS, TestOutputList (
+      [["write_file"; "/new"; "hello\nworld\n"; "0"];
+       ["strings_e"; "b"; "/new"]], []);
+    (*InitBasicFS, TestOutputList (
+      [["write_file"; "/new"; "\000h\000e\000l\000l\000o\000\n\000w\000o\000r\000l\000d\000\n"; "24"];
+       ["strings_e"; "b"; "/new"]], ["hello"; "world"])*)],
+   "print the printable strings in a file",
+   "\
+This is like the C<guestfs_strings> command, but allows you to
+specify the encoding.
+
+See the L<strings(1)> manpage for the full list of encodings.
+
+Commonly useful encodings are C<l> (lower case L) which will
+show strings inside Windows/x86 files.
+
+The returned strings are transcoded to UTF-8.");
+
+  ("hexdump", (RString "dump", [String "path"]), 96, [ProtocolLimitWarning],
+   [InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "hello\nworld\n"; "12"];
+       ["hexdump"; "/new"]], "00000000  68 65 6c 6c 6f 0a 77 6f  72 6c 64 0a              |hello.world.|\n0000000c\n")],
+   "dump a file in hexadecimal",
+   "\
+This runs C<hexdump -C> on the given C<path>.  The result is
+the human-readable, canonical hex dump of the file.");
 
 ]
 
@@ -1467,6 +2044,13 @@ let rec string_split sep str =
     s' :: string_split sep s''
   )
 
+let files_equal n1 n2 =
+  let cmd = sprintf "cmp -s %s %s" (Filename.quote n1) (Filename.quote n2) in
+  match Sys.command cmd with
+  | 0 -> true
+  | 1 -> false
+  | i -> failwithf "%s: failed with error code %d" cmd i
+
 let rec find_map f = function
   | [] -> raise Not_found
   | x :: xs ->
@@ -1630,14 +2214,15 @@ let chan = ref stdout
 let pr fs = ksprintf (output_string !chan) fs
 
 (* Generate a header block in a number of standard styles. *)
-type comment_style = CStyle | HashStyle | OCamlStyle
+type comment_style = CStyle | HashStyle | OCamlStyle | HaskellStyle
 type license = GPLv2 | LGPLv2
 
 let generate_header comment license =
   let c = match comment with
     | CStyle ->     pr "/* "; " *"
     | HashStyle ->  pr "# ";  "#"
-    | OCamlStyle -> pr "(* "; " *" in
+    | OCamlStyle -> pr "(* "; " *"
+    | HaskellStyle -> pr "{- "; "  " in
   pr "libguestfs generated file\n";
   pr "%s WARNING: THIS FILE IS GENERATED BY 'src/generator.ml'.\n" c;
   pr "%s ANY CHANGES YOU MAKE TO THIS FILE WILL BE LOST.\n" c;
@@ -1679,6 +2264,7 @@ let generate_header comment license =
    | CStyle -> pr " */\n"
    | HashStyle -> ()
    | OCamlStyle -> pr " *)\n"
+   | HaskellStyle -> pr "-}\n"
   );
   pr "\n"
 
@@ -1926,9 +2512,17 @@ and generate_xdr () =
 
   (* Message header, etc. *)
   pr "\
+/* The communication protocol is now documented in the guestfs(3)
+ * manpage.
+ */
+
 const GUESTFS_PROGRAM = 0x2000F5F5;
 const GUESTFS_PROTOCOL_VERSION = 1;
 
+/* These constants must be larger than any possible message length. */
+const GUESTFS_LAUNCH_FLAG = 0xf5f55ff5;
+const GUESTFS_CANCEL_FLAG = 0xffffeeee;
+
 enum guestfs_message_direction {
   GUESTFS_DIRECTION_CALL = 0,        /* client -> daemon */
   GUESTFS_DIRECTION_REPLY = 1        /* daemon -> client */
@@ -1945,19 +2539,6 @@ struct guestfs_message_error {
   string error_message<GUESTFS_ERROR_LEN>;
 };
 
-/* For normal requests and replies (not involving any FileIn or
- * FileOut parameters), the protocol is:
- *
- * For requests:
- *   total length (header + args, but not including length word itself)
- *   header
- *   guestfs_foo_args struct
- * For replies:
- *   total length (as above)
- *   header
- *   guestfs_foo_ret struct
- */
-
 struct guestfs_message_header {
   unsigned prog;                     /* GUESTFS_PROGRAM */
   unsigned vers;                     /* GUESTFS_PROTOCOL_VERSION */
@@ -1967,23 +2548,6 @@ struct guestfs_message_header {
   guestfs_message_status status;
 };
 
-/* Chunked encoding used to transfer files, for FileIn and FileOut
- * parameters.
- *
- * For requests which have >= 1 FileIn parameter:
- *   length of header + args (but not length word itself, and not chunks)
- *   header
- *   guestfs_foo_args struct
- *   sequence of chunks for FileIn param #0
- *   sequence of chunks for FileIn param #1 etc
- *
- * For replies which have >= 1 FileOut parameter:
- *   length of header + ret (but not length word itself, and not chunks)
- *   header
- *   guestfs_foo_ret struct
- *   sequence of chunks for FileOut param #0
- *   sequence of chunks for FileOut param #1 etc
- */
 const GUESTFS_MAX_CHUNK_SIZE = 8192;
 
 struct guestfs_chunk {
@@ -2139,11 +2703,15 @@ check_state (guestfs_h *g, const char *caller)
     fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
 
-      (* Generate the state struct which stores the high-level
+      (* Generate the context struct which stores the high-level
        * state between callback functions.
        *)
-      pr "struct %s_state {\n" shortname;
-      pr "  int cb_state;\n";
+      pr "struct %s_ctx {\n" shortname;
+      pr "  /* This flag is set by the callbacks, so we know we've done\n";
+      pr "   * the callbacks as expected, and in the right sequence.\n";
+      pr "   * 0 = not called, 1 = reply_cb called.\n";
+      pr "   */\n";
+      pr "  int cb_sequence;\n";
       pr "  struct guestfs_message_header hdr;\n";
       pr "  struct guestfs_message_error err;\n";
       (match fst style with
@@ -2161,19 +2729,29 @@ check_state (guestfs_h *g, const char *caller)
       pr "};\n";
       pr "\n";
 
-      (* Generate the callback function. *)
-      pr "static void %s_cb (guestfs_h *g, void *data, XDR *xdr)\n" shortname;
+      (* Generate the reply callback function. *)
+      pr "static void %s_reply_cb (guestfs_h *g, void *data, XDR *xdr)\n" shortname;
       pr "{\n";
       pr "  guestfs_main_loop *ml = guestfs_get_main_loop (g);\n";
-      pr "  struct %s_state *state = (struct %s_state *) data;\n" shortname shortname;
+      pr "  struct %s_ctx *ctx = (struct %s_ctx *) data;\n" shortname shortname;
+      pr "\n";
+      pr "  /* This should definitely not happen. */\n";
+      pr "  if (ctx->cb_sequence != 0) {\n";
+      pr "    ctx->cb_sequence = 9999;\n";
+      pr "    error (g, \"%%s: internal error: reply callback called twice\", \"%s\");\n" name;
+      pr "    return;\n";
+      pr "  }\n";
       pr "\n";
-      pr "  if (!xdr_guestfs_message_header (xdr, &state->hdr)) {\n";
-      pr "    error (g, \"%s: failed to parse reply header\");\n" name;
+      pr "  ml->main_loop_quit (ml, g);\n";
+      pr "\n";
+      pr "  if (!xdr_guestfs_message_header (xdr, &ctx->hdr)) {\n";
+      pr "    error (g, \"%%s: failed to parse reply header\", \"%s\");\n" name;
       pr "    return;\n";
       pr "  }\n";
-      pr "  if (state->hdr.status == GUESTFS_STATUS_ERROR) {\n";
-      pr "    if (!xdr_guestfs_message_error (xdr, &state->err)) {\n";
-      pr "      error (g, \"%s: failed to parse reply error\");\n" name;
+      pr "  if (ctx->hdr.status == GUESTFS_STATUS_ERROR) {\n";
+      pr "    if (!xdr_guestfs_message_error (xdr, &ctx->err)) {\n";
+      pr "      error (g, \"%%s: failed to parse reply error\", \"%s\");\n"
+       name;
       pr "      return;\n";
       pr "    }\n";
       pr "    goto done;\n";
@@ -2189,15 +2767,14 @@ check_state (guestfs_h *g, const char *caller)
        | RPVList _ | RVGList _ | RLVList _
        | RStat _ | RStatVFS _
        | RHashtable _ ->
-           pr "  if (!xdr_%s_ret (xdr, &state->ret)) {\n" name;
-           pr "    error (g, \"%s: failed to parse reply\");\n" name;
+           pr "  if (!xdr_%s_ret (xdr, &ctx->ret)) {\n" name;
+           pr "    error (g, \"%%s: failed to parse reply\", \"%s\");\n" name;
            pr "    return;\n";
            pr "  }\n";
       );
 
       pr " done:\n";
-      pr "  state->cb_state = 1;\n";
-      pr "  ml->main_loop_quit (ml, g);\n";
+      pr "  ctx->cb_sequence = 1;\n";
       pr "}\n\n";
 
       (* Generate the action stub. *)
@@ -2222,19 +2799,20 @@ check_state (guestfs_h *g, const char *caller)
        | _ -> pr "  struct %s_args args;\n" name
       );
 
-      pr "  struct %s_state state;\n" shortname;
+      pr "  struct %s_ctx ctx;\n" shortname;
       pr "  guestfs_main_loop *ml = guestfs_get_main_loop (g);\n";
       pr "  int serial;\n";
       pr "\n";
       pr "  if (check_state (g, \"%s\") == -1) return %s;\n" name error_code;
+      pr "  guestfs_set_busy (g);\n";
       pr "\n";
-      pr "  memset (&state, 0, sizeof state);\n";
+      pr "  memset (&ctx, 0, sizeof ctx);\n";
       pr "\n";
 
-      (* Dispatch the main header and arguments. *)
+      (* Send the main header and arguments. *)
       (match snd style with
        | [] ->
-          pr "  serial = guestfs_send (g, GUESTFS_PROC_%s, NULL, NULL);\n"
+          pr "  serial = guestfs__send_sync (g, GUESTFS_PROC_%s, NULL, NULL);\n"
             (String.uppercase shortname)
        | args ->
           List.iter (
@@ -2252,43 +2830,63 @@ check_state (guestfs_h *g, const char *caller)
                 pr "  args.%s = %s;\n" n n
             | FileIn _ | FileOut _ -> ()
           ) args;
-          pr "  serial = guestfs_send (g, GUESTFS_PROC_%s,\n"
+          pr "  serial = guestfs__send_sync (g, GUESTFS_PROC_%s,\n"
             (String.uppercase shortname);
-          pr "                     (xdrproc_t) xdr_%s_args, (char *) &args);\n"
+          pr "        (xdrproc_t) xdr_%s_args, (char *) &args);\n"
             name;
       );
-      pr "  if (serial == -1)\n";
+      pr "  if (serial == -1) {\n";
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
+      pr "  }\n";
       pr "\n";
 
-      (* Send any additional files requested. *)
+      (* Send any additional files (FileIn) requested. *)
+      let need_read_reply_label = ref false in
       List.iter (
        function
        | FileIn n ->
-           pr "  if (send_file (g, %s) == -1)\n" n;
-           pr "    return %s;\n" error_code;
+           pr "  {\n";
+           pr "    int r;\n";
+           pr "\n";
+           pr "    r = guestfs__send_file_sync (g, %s);\n" n;
+           pr "    if (r == -1) {\n";
+           pr "      guestfs_end_busy (g);\n";
+           pr "      return %s;\n" error_code;
+           pr "    }\n";
+           pr "    if (r == -2) /* daemon cancelled */\n";
+           pr "      goto read_reply;\n";
+           need_read_reply_label := true;
+           pr "  }\n";
            pr "\n";
        | _ -> ()
       ) (snd style);
 
       (* Wait for the reply from the remote end. *)
-      pr "  state.cb_state = 0;\n";
-      pr "  guestfs_set_reply_callback (g, %s_cb, &state);\n" shortname;
+      if !need_read_reply_label then pr " read_reply:\n";
+      pr "  guestfs__switch_to_receiving (g);\n";
+      pr "  ctx.cb_sequence = 0;\n";
+      pr "  guestfs_set_reply_callback (g, %s_reply_cb, &ctx);\n" shortname;
       pr "  (void) ml->main_loop_run (ml, g);\n";
       pr "  guestfs_set_reply_callback (g, NULL, NULL);\n";
-      pr "  if (!state.cb_state) {\n";
-      pr "    error (g, \"%s failed, see earlier error messages\");\n" name;
+      pr "  if (ctx.cb_sequence != 1) {\n";
+      pr "    error (g, \"%%s reply failed, see earlier error messages\", \"%s\");\n" name;
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
 
-      pr "  if (check_reply_header (g, &state.hdr, GUESTFS_PROC_%s, serial) == -1)\n"
+      pr "  if (check_reply_header (g, &ctx.hdr, GUESTFS_PROC_%s, serial) == -1) {\n"
        (String.uppercase shortname);
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
+      pr "  }\n";
       pr "\n";
 
-      pr "  if (state.hdr.status == GUESTFS_STATUS_ERROR) {\n";
-      pr "    error (g, \"%%s\", state.err.error_message);\n";
+      pr "  if (ctx.hdr.status == GUESTFS_STATUS_ERROR) {\n";
+      pr "    error (g, \"%%s\", ctx.err.error_message);\n";
+      pr "    free (ctx.err.error_message);\n";
+      pr "    guestfs_end_busy (g);\n";
       pr "    return %s;\n" error_code;
       pr "  }\n";
       pr "\n";
@@ -2297,35 +2895,39 @@ check_state (guestfs_h *g, const char *caller)
       List.iter (
        function
        | FileOut n ->
-           pr "  if (receive_file (g, %s) == -1)\n" n;
+           pr "  if (guestfs__receive_file_sync (g, %s) == -1) {\n" n;
+           pr "    guestfs_end_busy (g);\n";
            pr "    return %s;\n" error_code;
+           pr "  }\n";
            pr "\n";
        | _ -> ()
       ) (snd style);
 
+      pr "  guestfs_end_busy (g);\n";
+
       (match fst style with
        | RErr -> pr "  return 0;\n"
        | RInt n | RInt64 n | RBool n ->
-          pr "  return state.ret.%s;\n" n
+          pr "  return ctx.ret.%s;\n" n
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
        | RString n ->
-          pr "  return state.ret.%s; /* caller will free */\n" n
+          pr "  return ctx.ret.%s; /* caller will free */\n" n
        | RStringList n | RHashtable n ->
           pr "  /* caller will free this, but we need to add a NULL entry */\n";
-          pr "  state.ret.%s.%s_val =\n" n n;
-          pr "    safe_realloc (g, state.ret.%s.%s_val,\n" n n;
-          pr "                  sizeof (char *) * (state.ret.%s.%s_len + 1));\n"
+          pr "  ctx.ret.%s.%s_val =\n" n n;
+          pr "    safe_realloc (g, ctx.ret.%s.%s_val,\n" n n;
+          pr "                  sizeof (char *) * (ctx.ret.%s.%s_len + 1));\n"
             n n;
-          pr "  state.ret.%s.%s_val[state.ret.%s.%s_len] = NULL;\n" n n n n;
-          pr "  return state.ret.%s.%s_val;\n" n n
+          pr "  ctx.ret.%s.%s_val[ctx.ret.%s.%s_len] = NULL;\n" n n n n;
+          pr "  return ctx.ret.%s.%s_val;\n" n n
        | RIntBool _ ->
           pr "  /* caller with free this */\n";
-          pr "  return safe_memdup (g, &state.ret, sizeof (state.ret));\n"
+          pr "  return safe_memdup (g, &ctx.ret, sizeof (ctx.ret));\n"
        | RPVList n | RVGList n | RLVList n
        | RStat n | RStatVFS n ->
           pr "  /* caller will free this */\n";
-          pr "  return safe_memdup (g, &state.ret.%s, sizeof (state.ret.%s));\n" n n
+          pr "  return safe_memdup (g, &ctx.ret.%s, sizeof (ctx.ret.%s));\n" n n
       );
 
       pr "}\n\n"
@@ -2349,7 +2951,7 @@ and generate_daemon_actions_h () =
 and generate_daemon_actions () =
   generate_header CStyle GPLv2;
 
-  pr "#define _GNU_SOURCE // for strchrnul\n";
+  pr "#include <config.h>\n";
   pr "\n";
   pr "#include <stdio.h>\n";
   pr "#include <stdlib.h>\n";
@@ -2415,9 +3017,14 @@ and generate_daemon_actions () =
             | String n -> pr "  %s = args.%s;\n" n n
             | OptString n -> pr "  %s = args.%s ? *args.%s : NULL;\n" n n n
             | StringList n ->
-                pr "  args.%s.%s_val = realloc (args.%s.%s_val, sizeof (char *) * (args.%s.%s_len+1));\n" n n n n n n;
-                pr "  args.%s.%s_val[args.%s.%s_len] = NULL;\n" n n n n;
-                pr "  %s = args.%s.%s_val;\n" n n n
+                pr "  %s = realloc (args.%s.%s_val,\n" n n n;
+                pr "                sizeof (char *) * (args.%s.%s_len+1));\n" n n;
+                pr "  if (%s == NULL) {\n" n;
+                pr "    reply_with_perror (\"realloc\");\n";
+                pr "    goto done;\n";
+                pr "  }\n";
+                pr "  %s[args.%s.%s_len] = NULL;\n" n n n;
+                pr "  args.%s.%s_val = %s;\n" n n n;
             | Bool n -> pr "  %s = args.%s;\n" n n
             | Int n -> pr "  %s = args.%s;\n" n n
             | FileIn _ | FileOut _ -> ()
@@ -2698,6 +3305,11 @@ and generate_tests () =
 static guestfs_h *g;
 static int suppress_error = 0;
 
+/* This will be 's' or 'h' depending on whether the guest kernel
+ * names IDE devices /dev/sd* or /dev/hd*.
+ */
+static char devchar = 's';
+
 static void print_error (guestfs_h *g, void *data, const char *msg)
 {
   if (!suppress_error)
@@ -2755,9 +3367,10 @@ int main (int argc, char *argv[])
   char c = 0;
   int failed = 0;
   const char *srcdir;
-  int fd;
-  char buf[256];
+  const char *filename;
+  int fd, i;
   int nr_tests, test_num = 0;
+  char **devs;
 
   no_test_warnings ();
 
@@ -2771,89 +3384,90 @@ int main (int argc, char *argv[])
 
   srcdir = getenv (\"srcdir\");
   if (!srcdir) srcdir = \".\";
-  guestfs_set_path (g, srcdir);
+  chdir (srcdir);
+  guestfs_set_path (g, \".\");
 
-  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);
-  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  filename = \"test1.img\";
+  fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
   if (fd == -1) {
-    perror (buf);
+    perror (filename);
     exit (1);
   }
   if (lseek (fd, %d, SEEK_SET) == -1) {
     perror (\"lseek\");
     close (fd);
-    unlink (buf);
+    unlink (filename);
     exit (1);
   }
   if (write (fd, &c, 1) == -1) {
     perror (\"write\");
     close (fd);
-    unlink (buf);
+    unlink (filename);
     exit (1);
   }
   if (close (fd) == -1) {
-    perror (buf);
-    unlink (buf);
+    perror (filename);
+    unlink (filename);
     exit (1);
   }
-  if (guestfs_add_drive (g, buf) == -1) {
-    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+  if (guestfs_add_drive (g, filename) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", filename);
     exit (1);
   }
 
-  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);
-  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  filename = \"test2.img\";
+  fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
   if (fd == -1) {
-    perror (buf);
+    perror (filename);
     exit (1);
   }
   if (lseek (fd, %d, SEEK_SET) == -1) {
     perror (\"lseek\");
     close (fd);
-    unlink (buf);
+    unlink (filename);
     exit (1);
   }
   if (write (fd, &c, 1) == -1) {
     perror (\"write\");
     close (fd);
-    unlink (buf);
+    unlink (filename);
     exit (1);
   }
   if (close (fd) == -1) {
-    perror (buf);
-    unlink (buf);
+    perror (filename);
+    unlink (filename);
     exit (1);
   }
-  if (guestfs_add_drive (g, buf) == -1) {
-    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+  if (guestfs_add_drive (g, filename) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", filename);
     exit (1);
   }
 
-  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);
-  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  filename = \"test3.img\";
+  fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
   if (fd == -1) {
-    perror (buf);
+    perror (filename);
     exit (1);
   }
   if (lseek (fd, %d, SEEK_SET) == -1) {
     perror (\"lseek\");
     close (fd);
-    unlink (buf);
+    unlink (filename);
     exit (1);
   }
   if (write (fd, &c, 1) == -1) {
     perror (\"write\");
     close (fd);
-    unlink (buf);
+    unlink (filename);
     exit (1);
   }
   if (close (fd) == -1) {
-    perror (buf);
-    unlink (buf);
+    perror (filename);
+    unlink (filename);
     exit (1);
   }
-  if (guestfs_add_drive (g, buf) == -1) {
-    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+  if (guestfs_add_drive (g, filename) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", filename);
     exit (1);
   }
 
@@ -2866,6 +3480,28 @@ int main (int argc, char *argv[])
     exit (1);
   }
 
+  /* Detect if the appliance uses /dev/sd* or /dev/hd* in device
+   * names.  This changed between RHEL 5 and RHEL 6 so we have to
+   * support both.
+   */
+  devs = guestfs_list_devices (g);
+  if (devs == NULL || devs[0] == NULL) {
+    printf (\"guestfs_list_devices FAILED\\n\");
+    exit (1);
+  }
+  if (strncmp (devs[0], \"/dev/sd\", 7) == 0)
+    devchar = 's';
+  else if (strncmp (devs[0], \"/dev/hd\", 7) == 0)
+    devchar = 'h';
+  else {
+    printf (\"guestfs_list_devices returned unexpected string '%%s'\\n\",
+            devs[0]);
+    exit (1);
+  }
+  for (i = 0; devs[i] != NULL; ++i)
+    free (devs[i]);
+  free (devs);
+
   nr_tests = %d;
 
 " (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024) nr_tests;
@@ -2882,12 +3518,9 @@ int main (int argc, char *argv[])
   pr "\n";
 
   pr "  guestfs_close (g);\n";
-  pr "  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);\n";
-  pr "  unlink (buf);\n";
-  pr "  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);\n";
-  pr "  unlink (buf);\n";
-  pr "  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);\n";
-  pr "  unlink (buf);\n";
+  pr "  unlink (\"test1.img\");\n";
+  pr "  unlink (\"test2.img\");\n";
+  pr "  unlink (\"test3.img\");\n";
   pr "\n";
 
   pr "  if (failed > 0) {\n";
@@ -2910,12 +3543,14 @@ and generate_one_test name i (init, test) =
    | InitEmpty ->
        pr "  /* InitEmpty for %s (%d) */\n" name i;
        List.iter (generate_test_command_call test_name)
-        [["umount_all"];
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
          ["lvm_remove_all"]]
    | InitBasicFS ->
        pr "  /* InitBasicFS for %s (%d): create ext2 on /dev/sda1 */\n" name i;
        List.iter (generate_test_command_call test_name)
-        [["umount_all"];
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
          ["lvm_remove_all"];
          ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
          ["mkfs"; "ext2"; "/dev/sda1"];
@@ -2924,7 +3559,8 @@ and generate_one_test name i (init, test) =
        pr "  /* InitBasicFSonLVM for %s (%d): create ext2 on /dev/VG/LV */\n"
         name i;
        List.iter (generate_test_command_call test_name)
-        [["umount_all"];
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
          ["lvm_remove_all"];
          ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
          ["pvcreate"; "/dev/sda1"];
@@ -2949,10 +3585,14 @@ and generate_one_test name i (init, test) =
        List.iter (generate_test_command_call test_name) seq
    | TestOutput (seq, expected) ->
        pr "  /* TestOutput for %s (%d) */\n" name i;
+       pr "  char expected[] = \"%s\";\n" (c_quote expected);
+       if String.length expected > 7 &&
+          String.sub expected 0 7 = "/dev/sd" then
+        pr "  expected[5] = devchar;\n";
        let seq, last = get_seq_last seq in
        let test () =
-        pr "    if (strcmp (r, \"%s\") != 0) {\n" (c_quote expected);
-        pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r);\n" test_name (c_quote expected);
+        pr "    if (strcmp (r, expected) != 0) {\n";
+        pr "      fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r);\n" test_name;
         pr "      return -1;\n";
         pr "    }\n"
        in
@@ -2969,9 +3609,14 @@ and generate_one_test name i (init, test) =
             pr "      print_strings (r);\n";
             pr "      return -1;\n";
             pr "    }\n";
-            pr "    if (strcmp (r[%d], \"%s\") != 0) {\n" i (c_quote str);
-            pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r[%d]);\n" test_name (c_quote str) i;
-            pr "      return -1;\n";
+             pr "    {\n";
+             pr "      char expected[] = \"%s\";\n" (c_quote str);
+             if String.length str > 7 && String.sub str 0 7 = "/dev/sd" then
+              pr "      expected[5] = devchar;\n";
+            pr "      if (strcmp (r[%d], expected) != 0) {\n" i;
+            pr "        fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r[%d]);\n" test_name i;
+            pr "        return -1;\n";
+            pr "      }\n";
             pr "    }\n"
         ) expected;
         pr "    if (r[%d] != NULL) {\n" (List.length expected);
@@ -3115,16 +3760,26 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
 
       List.iter (
        function
-       | String _, _
-       | OptString _, _
+       | OptString n, "NULL" -> ()
+       | String n, arg
+       | OptString n, arg ->
+           pr "    char %s[] = \"%s\";\n" n (c_quote arg);
+           if String.length arg > 7 && String.sub arg 0 7 = "/dev/sd" then
+             pr "    %s[5] = devchar;\n" n
        | Int _, _
-       | Bool _, _ -> ()
+       | Bool _, _
        | FileIn _, _ | FileOut _, _ -> ()
        | StringList n, arg ->
-           pr "    char *%s[] = {\n" n;
            let strs = string_split " " arg in
-           List.iter (
-             fun str -> pr "      \"%s\",\n" (c_quote str)
+           iteri (
+             fun i str ->
+                pr "    char %s_%d[] = \"%s\";\n" n i (c_quote str);
+               if String.length str > 7 && String.sub str 0 7 = "/dev/sd" then
+                 pr "    %s_%d[5] = devchar;\n" n i
+           ) strs;
+           pr "    char *%s[] = {\n" n;
+           iteri (
+             fun i _ -> pr "      %s_%d,\n" n i
            ) strs;
            pr "      NULL\n";
            pr "    };\n";
@@ -3159,11 +3814,12 @@ and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
       (* Generate the parameters. *)
       List.iter (
        function
-       | String _, arg
+       | OptString _, "NULL" -> pr ", NULL"
+       | String n, _
+       | OptString n, _ ->
+            pr ", %s" n
        | FileIn _, arg | FileOut _, arg ->
            pr ", \"%s\"" (c_quote arg)
-       | OptString _, arg ->
-           if arg = "NULL" then pr ", NULL" else pr ", \"%s\"" (c_quote arg)
        | StringList n, _ ->
            pr ", %s" n
        | Int _, arg ->
@@ -3214,6 +3870,7 @@ and c_quote str =
   let str = replace_str str "\r" "\\r" in
   let str = replace_str str "\n" "\\n" in
   let str = replace_str str "\t" "\\t" in
+  let str = replace_str str "\000" "\\0" in
   str
 
 (* Generate a lot of different functions for guestfish. *)
@@ -3549,7 +4206,7 @@ and generate_fish_completion () =
 
 #ifdef HAVE_LIBREADLINE
 
-static const char *commands[] = {
+static const char *const commands[] = {
 ";
 
   (* Get the commands and sort them, including the aliases. *)
@@ -3613,9 +4270,19 @@ and generate_fish_actions_pod () =
       fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
     ) all_functions_sorted in
 
+  let rex = Str.regexp "C<guestfs_\\([^>]+\\)>" in
+
   List.iter (
     fun (name, style, _, flags, _, _, longdesc) ->
-      let longdesc = replace_str longdesc "C<guestfs_" "C<" in
+      let longdesc =
+       Str.global_substitute rex (
+         fun s ->
+           let sub =
+             try Str.matched_group 1 s
+             with Not_found ->
+               failwithf "error substituting C<guestfs_...> in longdesc of function %s" name in
+           "C<" ^ replace_char sub '_' '-' ^ ">"
+       ) longdesc in
       let name = replace_char name '_' '-' in
       let alias =
        try find_map (function FishAlias n -> Some n | _ -> None) flags
@@ -3631,7 +4298,7 @@ and generate_fish_actions_pod () =
        function
        | String n -> pr " %s" n
        | OptString n -> pr " %s" n
-       | StringList n -> pr " %s,..." n
+       | StringList n -> pr " '%s ...'" n
        | Bool _ -> pr " true|false"
        | Int n -> pr " %s" n
        | FileIn n | FileOut n -> pr " (%s|-)" n
@@ -3941,6 +4608,8 @@ copy_table (char * const * argv)
       pr "{\n";
 
       (match params with
+       | [p1; p2; p3; p4; p5] ->
+          pr "  CAMLparam5 (%s);\n" (String.concat ", " params)
        | p1 :: p2 :: p3 :: p4 :: p5 :: rest ->
           pr "  CAMLparam5 (%s);\n" (String.concat ", " [p1; p2; p3; p4; p5]);
           pr "  CAMLxparam%d (%s);\n"
@@ -3967,7 +4636,7 @@ copy_table (char * const * argv)
            pr "    %sv != Val_int (0) ? String_val (Field (%sv, 0)) : NULL;\n"
              n n
        | StringList n ->
-           pr "  char **%s = ocaml_guestfs_strings_val (%sv);\n" n n
+           pr "  char **%s = ocaml_guestfs_strings_val (g, %sv);\n" n n
        | Bool n ->
            pr "  int %s = Bool_val (%sv);\n" n n
        | Int n ->
@@ -4189,12 +4858,13 @@ XS_unpack_charPtrPtr (SV *arg) {
   AV *av;
   I32 i;
 
-  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV) {
+  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV)
     croak (\"array reference expected\");
-  }
 
   av = (AV *)SvRV (arg);
-  ret = (char **)malloc (av_len (av) + 1 + 1);
+  ret = malloc ((av_len (av) + 1 + 1) * sizeof (char *));
+  if (!ret)
+    croak (\"malloc failed\");
 
   for (i = 0; i <= av_len (av); i++) {
     SV **elem = av_fetch (av, i, 0);
@@ -4212,6 +4882,8 @@ XS_unpack_charPtrPtr (SV *arg) {
 
 MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
 
+PROTOTYPES: ENABLE
+
 guestfs_h *
 _create ()
    CODE:
@@ -5079,6 +5751,8 @@ class GuestFS:
 
 (* Useful if you need the longdesc POD text as plain text.  Returns a
  * list of lines.
+ *
+ * This is the slowest thing about autogeneration.
  *)
 and pod2text ~width name longdesc =
   let filename, chan = Filename.open_temp_file "gen" ".tmp" in
@@ -5119,6 +5793,11 @@ and generate_ruby_c () =
 
 #include \"extconf.h\"
 
+/* For Ruby < 1.9 */
+#ifndef RARRAY_LEN
+#define RARRAY_LEN(r) (RARRAY((r))->len)
+#endif
+
 static VALUE m_guestfs;                        /* guestfs module */
 static VALUE c_guestfs;                        /* guestfs_h handle */
 static VALUE e_Error;                  /* used for all errors */
@@ -5186,11 +5865,13 @@ static VALUE ruby_guestfs_close (VALUE gv)
            pr "  {\n";
            pr "    int i, len;\n";
            pr "    len = RARRAY_LEN (%sv);\n" n;
-           pr "    %s = malloc (sizeof (char *) * (len+1));\n" n;
+           pr "    %s = guestfs_safe_malloc (g, sizeof (char *) * (len+1));\n"
+             n;
            pr "    for (i = 0; i < len; ++i) {\n";
            pr "      VALUE v = rb_ary_entry (%sv, i);\n" n;
            pr "      %s[i] = StringValueCStr (v);\n" n;
            pr "    }\n";
+           pr "    %s[len] = NULL;\n" n;
            pr "  }\n";
        | Bool n
        | Int n ->
@@ -5341,14 +6022,732 @@ and generate_ruby_lvm_code typ cols =
   pr "  guestfs_free_lvm_%s_list (r);\n" typ;
   pr "  return rv;\n"
 
+(* Generate Java bindings GuestFS.java file. *)
+and generate_java_java () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+package com.redhat.et.libguestfs;
+
+import java.util.HashMap;
+import com.redhat.et.libguestfs.LibGuestFSException;
+import com.redhat.et.libguestfs.PV;
+import com.redhat.et.libguestfs.VG;
+import com.redhat.et.libguestfs.LV;
+import com.redhat.et.libguestfs.Stat;
+import com.redhat.et.libguestfs.StatVFS;
+import com.redhat.et.libguestfs.IntBool;
+
+/**
+ * The GuestFS object is a libguestfs handle.
+ *
+ * @author rjones
+ */
+public class GuestFS {
+  // Load the native code.
+  static {
+    System.loadLibrary (\"guestfs_jni\");
+  }
+
+  /**
+   * The native guestfs_h pointer.
+   */
+  long g;
+
+  /**
+   * Create a libguestfs handle.
+   *
+   * @throws LibGuestFSException
+   */
+  public GuestFS () throws LibGuestFSException
+  {
+    g = _create ();
+  }
+  private native long _create () throws LibGuestFSException;
+
+  /**
+   * Close a libguestfs handle.
+   *
+   * You can also leave handles to be collected by the garbage
+   * collector, but this method ensures that the resources used
+   * by the handle are freed up immediately.  If you call any
+   * other methods after closing the handle, you will get an
+   * exception.
+   *
+   * @throws LibGuestFSException
+   */
+  public void close () throws LibGuestFSException
+  {
+    if (g != 0)
+      _close (g);
+    g = 0;
+  }
+  private native void _close (long g) throws LibGuestFSException;
+
+  public void finalize () throws LibGuestFSException
+  {
+    close ();
+  }
+
+";
+
+  List.iter (
+    fun (name, style, _, flags, _, shortdesc, longdesc) ->
+      let doc = replace_str longdesc "C<guestfs_" "C<g." in
+      let doc =
+       if List.mem ProtocolLimitWarning flags then
+         doc ^ "\n\n" ^ protocol_limit_warning
+       else doc in
+      let doc =
+       if List.mem DangerWillRobinson flags then
+         doc ^ "\n\n" ^ danger_will_robinson
+       else doc in
+      let doc = pod2text ~width:60 name doc in
+      let doc = String.concat "\n   * " doc in
+
+      pr "  /**\n";
+      pr "   * %s\n" shortdesc;
+      pr "   *\n";
+      pr "   * %s\n" doc;
+      pr "   * @throws LibGuestFSException\n";
+      pr "   */\n";
+      pr "  ";
+      generate_java_prototype ~public:true ~semicolon:false name style;
+      pr "\n";
+      pr "  {\n";
+      pr "    if (g == 0)\n";
+      pr "      throw new LibGuestFSException (\"%s: handle is closed\");\n"
+       name;
+      pr "    ";
+      if fst style <> RErr then pr "return ";
+      pr "_%s " name;
+      generate_call_args ~handle:"g" (snd style);
+      pr ";\n";
+      pr "  }\n";
+      pr "  ";
+      generate_java_prototype ~privat:true ~native:true name style;
+      pr "\n";
+      pr "\n";
+  ) all_functions;
+
+  pr "}\n"
+
+and generate_java_prototype ?(public=false) ?(privat=false) ?(native=false)
+    ?(semicolon=true) name style =
+  if privat then pr "private ";
+  if public then pr "public ";
+  if native then pr "native ";
+
+  (* return type *)
+  (match fst style with
+   | RErr -> pr "void ";
+   | RInt _ -> pr "int ";
+   | RInt64 _ -> pr "long ";
+   | RBool _ -> pr "boolean ";
+   | RConstString _ | RString _ -> pr "String ";
+   | RStringList _ -> pr "String[] ";
+   | RIntBool _ -> pr "IntBool ";
+   | RPVList _ -> pr "PV[] ";
+   | RVGList _ -> pr "VG[] ";
+   | RLVList _ -> pr "LV[] ";
+   | RStat _ -> pr "Stat ";
+   | RStatVFS _ -> pr "StatVFS ";
+   | RHashtable _ -> pr "HashMap<String,String> ";
+  );
+
+  if native then pr "_%s " name else pr "%s " name;
+  pr "(";
+  let needs_comma = ref false in
+  if native then (
+    pr "long g";
+    needs_comma := true
+  );
+
+  (* args *)
+  List.iter (
+    fun arg ->
+      if !needs_comma then pr ", ";
+      needs_comma := true;
+
+      match arg with
+      | String n
+      | OptString n
+      | FileIn n
+      | FileOut n ->
+         pr "String %s" n
+      | StringList n ->
+         pr "String[] %s" n
+      | Bool n ->
+         pr "boolean %s" n
+      | Int n ->
+         pr "int %s" n
+  ) (snd style);
+
+  pr ")\n";
+  pr "    throws LibGuestFSException";
+  if semicolon then pr ";"
+
+and generate_java_struct typ cols =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+package com.redhat.et.libguestfs;
+
+/**
+ * Libguestfs %s structure.
+ *
+ * @author rjones
+ * @see GuestFS
+ */
+public class %s {
+" typ typ;
+
+  List.iter (
+    function
+    | name, `String
+    | name, `UUID -> pr "  public String %s;\n" name
+    | name, `Bytes
+    | name, `Int -> pr "  public long %s;\n" name
+    | name, `OptPercent ->
+       pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
+       pr "  public float %s;\n" name
+  ) cols;
+
+  pr "}\n"
+
+and generate_java_c () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include \"com_redhat_et_libguestfs_GuestFS.h\"
+#include \"guestfs.h\"
+
+/* Note that this function returns.  The exception is not thrown
+ * until after the wrapper function returns.
+ */
+static void
+throw_exception (JNIEnv *env, const char *msg)
+{
+  jclass cl;
+  cl = (*env)->FindClass (env,
+                          \"com/redhat/et/libguestfs/LibGuestFSException\");
+  (*env)->ThrowNew (env, cl, msg);
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_redhat_et_libguestfs_GuestFS__1create
+  (JNIEnv *env, jobject obj)
+{
+  guestfs_h *g;
+
+  g = guestfs_create ();
+  if (g == NULL) {
+    throw_exception (env, \"GuestFS.create: failed to allocate handle\");
+    return 0;
+  }
+  guestfs_set_error_handler (g, NULL, NULL);
+  return (jlong) (long) g;
+}
+
+JNIEXPORT void JNICALL
+Java_com_redhat_et_libguestfs_GuestFS__1close
+  (JNIEnv *env, jobject obj, jlong jg)
+{
+  guestfs_h *g = (guestfs_h *) (long) jg;
+  guestfs_close (g);
+}
+
+";
+
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "JNIEXPORT ";
+      (match fst style with
+       | RErr -> pr "void ";
+       | RInt _ -> pr "jint ";
+       | RInt64 _ -> pr "jlong ";
+       | RBool _ -> pr "jboolean ";
+       | RConstString _ | RString _ -> pr "jstring ";
+       | RIntBool _ | RStat _ | RStatVFS _ | RHashtable _ ->
+          pr "jobject ";
+       | RStringList _ | RPVList _ | RVGList _ | RLVList _ ->
+          pr "jobjectArray ";
+      );
+      pr "JNICALL\n";
+      pr "Java_com_redhat_et_libguestfs_GuestFS_";
+      pr "%s" (replace_str ("_" ^ name) "_" "_1");
+      pr "\n";
+      pr "  (JNIEnv *env, jobject obj, jlong jg";
+      List.iter (
+       function
+       | String n
+       | OptString n
+       | FileIn n
+       | FileOut n ->
+           pr ", jstring j%s" n
+       | StringList n ->
+           pr ", jobjectArray j%s" n
+       | Bool n ->
+           pr ", jboolean j%s" n
+       | Int n ->
+           pr ", jint j%s" n
+      ) (snd style);
+      pr ")\n";
+      pr "{\n";
+      pr "  guestfs_h *g = (guestfs_h *) (long) jg;\n";
+      let error_code, no_ret =
+       match fst style with
+       | RErr -> pr "  int r;\n"; "-1", ""
+       | RBool _
+       | RInt _ -> pr "  int r;\n"; "-1", "0"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1", "0"
+       | RConstString _ -> pr "  const char *r;\n"; "NULL", "NULL"
+       | RString _ ->
+           pr "  jstring jr;\n";
+           pr "  char *r;\n"; "NULL", "NULL"
+       | RStringList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  int r_len;\n";
+           pr "  jclass cl;\n";
+           pr "  jstring jstr;\n";
+           pr "  char **r;\n"; "NULL", "NULL"
+       | RIntBool _ ->
+           pr "  jobject jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  struct guestfs_int_bool *r;\n"; "NULL", "NULL"
+       | RStat _ ->
+           pr "  jobject jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  struct guestfs_stat *r;\n"; "NULL", "NULL"
+       | RStatVFS _ ->
+           pr "  jobject jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  struct guestfs_statvfs *r;\n"; "NULL", "NULL"
+       | RPVList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  jobject jfl;\n";
+           pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL", "NULL"
+       | RVGList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  jobject jfl;\n";
+           pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL", "NULL"
+       | RLVList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  jobject jfl;\n";
+           pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL", "NULL"
+       | RHashtable _ -> pr "  char **r;\n"; "NULL", "NULL" in
+      List.iter (
+       function
+       | String n
+       | OptString n
+       | FileIn n
+       | FileOut n ->
+           pr "  const char *%s;\n" n
+       | StringList n ->
+           pr "  int %s_len;\n" n;
+           pr "  const char **%s;\n" n
+       | Bool n
+       | Int n ->
+           pr "  int %s;\n" n
+      ) (snd style);
+
+      let needs_i =
+       (match fst style with
+        | RStringList _ | RPVList _ | RVGList _ | RLVList _ -> true
+        | RErr | RBool _ | RInt _ | RInt64 _ | RConstString _
+        | RString _ | RIntBool _ | RStat _ | RStatVFS _
+        | RHashtable _ -> false) ||
+       List.exists (function StringList _ -> true | _ -> false) (snd style) in
+      if needs_i then
+       pr "  int i;\n";
+
+      pr "\n";
+
+      (* Get the parameters. *)
+      List.iter (
+       function
+       | String n
+       | OptString n
+       | FileIn n
+       | FileOut n ->
+           pr "  %s = (*env)->GetStringUTFChars (env, j%s, NULL);\n" n n
+       | StringList n ->
+           pr "  %s_len = (*env)->GetArrayLength (env, j%s);\n" n n;
+           pr "  %s = guestfs_safe_malloc (g, sizeof (char *) * (%s_len+1));\n" n n;
+           pr "  for (i = 0; i < %s_len; ++i) {\n" n;
+           pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
+             n;
+           pr "    %s[i] = (*env)->GetStringUTFChars (env, o, NULL);\n" n;
+           pr "  }\n";
+           pr "  %s[%s_len] = NULL;\n" n n;
+       | Bool n
+       | Int n ->
+           pr "  %s = j%s;\n" n n
+      ) (snd style);
+
+      (* Make the call. *)
+      pr "  r = guestfs_%s " name;
+      generate_call_args ~handle:"g" (snd style);
+      pr ";\n";
+
+      (* Release the parameters. *)
+      List.iter (
+       function
+       | String n
+       | OptString n
+       | FileIn n
+       | FileOut n ->
+           pr "  (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n n
+       | StringList n ->
+           pr "  for (i = 0; i < %s_len; ++i) {\n" n;
+           pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
+             n;
+           pr "    (*env)->ReleaseStringUTFChars (env, o, %s[i]);\n" n;
+           pr "  }\n";
+           pr "  free (%s);\n" n
+       | Bool n
+       | Int n -> ()
+      ) (snd style);
+
+      (* Check for errors. *)
+      pr "  if (r == %s) {\n" error_code;
+      pr "    throw_exception (env, guestfs_last_error (g));\n";
+      pr "    return %s;\n" no_ret;
+      pr "  }\n";
+
+      (* Return value. *)
+      (match fst style with
+       | RErr -> ()
+       | RInt _ -> pr "  return (jint) r;\n"
+       | RBool _ -> pr "  return (jboolean) r;\n"
+       | RInt64 _ -> pr "  return (jlong) r;\n"
+       | RConstString _ -> pr "  return (*env)->NewStringUTF (env, r);\n"
+       | RString _ ->
+          pr "  jr = (*env)->NewStringUTF (env, r);\n";
+          pr "  free (r);\n";
+          pr "  return jr;\n"
+       | RStringList _ ->
+          pr "  for (r_len = 0; r[r_len] != NULL; ++r_len) ;\n";
+          pr "  cl = (*env)->FindClass (env, \"java/lang/String\");\n";
+          pr "  jstr = (*env)->NewStringUTF (env, \"\");\n";
+          pr "  jr = (*env)->NewObjectArray (env, r_len, cl, jstr);\n";
+          pr "  for (i = 0; i < r_len; ++i) {\n";
+          pr "    jstr = (*env)->NewStringUTF (env, r[i]);\n";
+          pr "    (*env)->SetObjectArrayElement (env, jr, i, jstr);\n";
+          pr "    free (r[i]);\n";
+          pr "  }\n";
+          pr "  free (r);\n";
+          pr "  return jr;\n"
+       | RIntBool _ ->
+          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/IntBool\");\n";
+          pr "  jr = (*env)->AllocObject (env, cl);\n";
+          pr "  fl = (*env)->GetFieldID (env, cl, \"i\", \"I\");\n";
+          pr "  (*env)->SetIntField (env, jr, fl, r->i);\n";
+          pr "  fl = (*env)->GetFieldID (env, cl, \"i\", \"Z\");\n";
+          pr "  (*env)->SetBooleanField (env, jr, fl, r->b);\n";
+          pr "  guestfs_free_int_bool (r);\n";
+          pr "  return jr;\n"
+       | RStat _ ->
+          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/Stat\");\n";
+          pr "  jr = (*env)->AllocObject (env, cl);\n";
+          List.iter (
+            function
+            | name, `Int ->
+                pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n"
+                  name;
+                pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
+          ) stat_cols;
+          pr "  free (r);\n";
+          pr "  return jr;\n"
+       | RStatVFS _ ->
+          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/StatVFS\");\n";
+          pr "  jr = (*env)->AllocObject (env, cl);\n";
+          List.iter (
+            function
+            | name, `Int ->
+                pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n"
+                  name;
+                pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
+          ) statvfs_cols;
+          pr "  free (r);\n";
+          pr "  return jr;\n"
+       | RPVList _ ->
+          generate_java_lvm_return "pv" "PV" pv_cols
+       | RVGList _ ->
+          generate_java_lvm_return "vg" "VG" vg_cols
+       | RLVList _ ->
+          generate_java_lvm_return "lv" "LV" lv_cols
+       | RHashtable _ ->
+          (* XXX *)
+          pr "  throw_exception (env, \"%s: internal error: please let us know how to make a Java HashMap from JNI bindings!\");\n" name;
+          pr "  return NULL;\n"
+      );
+
+      pr "}\n";
+      pr "\n"
+  ) all_functions
+
+and generate_java_lvm_return typ jtyp cols =
+  pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/%s\");\n" jtyp;
+  pr "  jr = (*env)->NewObjectArray (env, r->len, cl, NULL);\n";
+  pr "  for (i = 0; i < r->len; ++i) {\n";
+  pr "    jfl = (*env)->AllocObject (env, cl);\n";
+  List.iter (
+    function
+    | name, `String ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
+       pr "    (*env)->SetObjectField (env, jfl, fl, (*env)->NewStringUTF (env, r->val[i].%s));\n" name;
+    | name, `UUID ->
+       pr "    {\n";
+       pr "      char s[33];\n";
+       pr "      memcpy (s, r->val[i].%s, 32);\n" name;
+       pr "      s[32] = 0;\n";
+       pr "      fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
+       pr "      (*env)->SetObjectField (env, jfl, fl, (*env)->NewStringUTF (env, s));\n";
+       pr "    }\n";
+    | name, (`Bytes|`Int) ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n" name;
+       pr "    (*env)->SetLongField (env, jfl, fl, r->val[i].%s);\n" name;
+    | name, `OptPercent ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"F\");\n" name;
+       pr "    (*env)->SetFloatField (env, jfl, fl, r->val[i].%s);\n" name;
+  ) cols;
+  pr "    (*env)->SetObjectArrayElement (env, jfl, i, jfl);\n";
+  pr "  }\n";
+  pr "  guestfs_free_lvm_%s_list (r);\n" typ;
+  pr "  return jr;\n"
+
+and generate_haskell_hs () =
+  generate_header HaskellStyle LGPLv2;
+
+  (* XXX We only know how to generate partial FFI for Haskell
+   * at the moment.  Please help out!
+   *)
+  let can_generate style =
+    let check_no_bad_args =
+      List.for_all (function Bool _ | Int _ -> false | _ -> true)
+    in
+    match style with
+    | RErr, args -> check_no_bad_args args
+    | RBool _, _
+    | RInt _, _
+    | RInt64 _, _
+    | RConstString _, _
+    | RString _, _
+    | RStringList _, _
+    | RIntBool _, _
+    | RPVList _, _
+    | RVGList _, _
+    | RLVList _, _
+    | RStat _, _
+    | RStatVFS _, _
+    | RHashtable _, _ -> false in
+
+  pr "\
+{-# INCLUDE <guestfs.h> #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module Guestfs (
+  create";
+
+  (* List out the names of the actions we want to export. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if can_generate style then pr ",\n  %s" name
+  ) all_functions;
+
+  pr "
+  ) where
+import Foreign
+import Foreign.C
+import IO
+import Control.Exception
+import Data.Typeable
+
+data GuestfsS = GuestfsS            -- represents the opaque C struct
+type GuestfsP = Ptr GuestfsS        -- guestfs_h *
+type GuestfsH = ForeignPtr GuestfsS -- guestfs_h * with attached finalizer
+
+-- XXX define properly later XXX
+data PV = PV
+data VG = VG
+data LV = LV
+data IntBool = IntBool
+data Stat = Stat
+data StatVFS = StatVFS
+data Hashtable = Hashtable
+
+foreign import ccall unsafe \"guestfs_create\" c_create
+  :: IO GuestfsP
+foreign import ccall unsafe \"&guestfs_close\" c_close
+  :: FunPtr (GuestfsP -> IO ())
+foreign import ccall unsafe \"guestfs_set_error_handler\" c_set_error_handler
+  :: GuestfsP -> Ptr CInt -> Ptr CInt -> IO ()
+
+create :: IO GuestfsH
+create = do
+  p <- c_create
+  c_set_error_handler p nullPtr nullPtr
+  h <- newForeignPtr c_close p
+  return h
+
+foreign import ccall unsafe \"guestfs_last_error\" c_last_error
+  :: GuestfsP -> IO CString
+
+-- last_error :: GuestfsH -> IO (Maybe String)
+-- last_error h = do
+--   str <- withForeignPtr h (\\p -> c_last_error p)
+--   maybePeek peekCString str
+
+last_error :: GuestfsH -> IO (String)
+last_error h = do
+  str <- withForeignPtr h (\\p -> c_last_error p)
+  if (str == nullPtr)
+    then return \"no error\"
+    else peekCString str
+
+";
+
+  (* Generate wrappers for each foreign function. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if can_generate style then (
+       pr "foreign import ccall unsafe \"guestfs_%s\" c_%s\n" name name;
+       pr "  :: ";
+       generate_haskell_prototype ~handle:"GuestfsP" style;
+       pr "\n";
+       pr "\n";
+       pr "%s :: " name;
+       generate_haskell_prototype ~handle:"GuestfsH" ~hs:true style;
+       pr "\n";
+       pr "%s %s = do\n" name
+         (String.concat " " ("h" :: List.map name_of_argt (snd style)));
+       pr "  r <- ";
+       List.iter (
+         function
+         | FileIn n
+         | FileOut n
+         | String n -> pr "withCString %s $ \\%s -> " n n
+         | OptString n -> pr "maybeWith withCString %s $ \\%s -> " n n
+         | StringList n -> pr "withMany withCString %s $ \\%s -> withArray0 nullPtr %s $ \\%s -> " n n n n
+         | Bool n ->
+             (* XXX this doesn't work *)
+             pr "      let\n";
+             pr "        %s = case %s of\n" n n;
+             pr "          False -> 0\n";
+             pr "          True -> 1\n";
+             pr "      in fromIntegral %s $ \\%s ->\n" n n
+         | Int n -> pr "fromIntegral %s $ \\%s -> " n n
+       ) (snd style);
+       pr "withForeignPtr h (\\p -> c_%s %s)\n" name
+         (String.concat " " ("p" :: List.map name_of_argt (snd style)));
+       (match fst style with
+        | RErr | RInt _ | RInt64 _ | RBool _ ->
+            pr "  if (r == -1)\n";
+            pr "    then do\n";
+            pr "      err <- last_error h\n";
+            pr "      fail err\n";
+        | RConstString _ | RString _ | RStringList _ | RIntBool _
+        | RPVList _ | RVGList _ | RLVList _ | RStat _ | RStatVFS _
+        | RHashtable _ ->
+            pr "  if (r == nullPtr)\n";
+            pr "    then do\n";
+            pr "      err <- last_error h\n";
+            pr "      fail err\n";
+       );
+       (match fst style with
+        | RErr ->
+            pr "    else return ()\n"
+        | RInt _ ->
+            pr "    else return (fromIntegral r)\n"
+        | RInt64 _ ->
+            pr "    else return (fromIntegral r)\n"
+        | RBool _ ->
+            pr "    else return (toBool r)\n"
+        | RConstString _
+        | RString _
+        | RStringList _
+        | RIntBool _
+        | RPVList _
+        | RVGList _
+        | RLVList _
+        | RStat _
+        | RStatVFS _
+        | RHashtable _ ->
+            pr "    else return ()\n" (* XXXXXXXXXXXXXXXXXXXX *)
+       );
+       pr "\n";
+      )
+  ) all_functions
+
+and generate_haskell_prototype ~handle ?(hs = false) style =
+  pr "%s -> " handle;
+  let string = if hs then "String" else "CString" in
+  let int = if hs then "Int" else "CInt" in
+  let bool = if hs then "Bool" else "CInt" in
+  let int64 = if hs then "Integer" else "Int64" in
+  List.iter (
+    fun arg ->
+      (match arg with
+       | String _ -> pr "%s" string
+       | OptString _ -> if hs then pr "Maybe String" else pr "CString"
+       | StringList _ -> if hs then pr "[String]" else pr "Ptr CString"
+       | Bool _ -> pr "%s" bool
+       | Int _ -> pr "%s" int
+       | FileIn _ -> pr "%s" string
+       | FileOut _ -> pr "%s" string
+      );
+      pr " -> ";
+  ) (snd style);
+  pr "IO (";
+  (match fst style with
+   | RErr -> if not hs then pr "CInt"
+   | RInt _ -> pr "%s" int
+   | RInt64 _ -> pr "%s" int64
+   | RBool _ -> pr "%s" bool
+   | RConstString _ -> pr "%s" string
+   | RString _ -> pr "%s" string
+   | RStringList _ -> pr "[%s]" string
+   | RIntBool _ -> pr "IntBool"
+   | RPVList _ -> pr "[PV]"
+   | RVGList _ -> pr "[VG]"
+   | RLVList _ -> pr "[LV]"
+   | RStat _ -> pr "Stat"
+   | RStatVFS _ -> pr "StatVFS"
+   | RHashtable _ -> pr "Hashtable"
+  );
+  pr ")"
+
 let output_to filename =
   let filename_new = filename ^ ".new" in
   chan := open_out filename_new;
   let close () =
     close_out !chan;
     chan := stdout;
-    Unix.rename filename_new filename;
-    printf "written %s\n%!" filename;
+
+    (* Is the new file different from the current file? *)
+    if Sys.file_exists filename && files_equal filename filename_new then
+      Unix.unlink filename_new         (* same, so skip it *)
+    else (
+      (* different, overwrite old one *)
+      (try Unix.chmod filename 0o644 with Unix.Unix_error _ -> ());
+      Unix.rename filename_new filename;
+      Unix.chmod filename 0o444;
+      printf "written %s\n%!" filename;
+    )
   in
   close
 
@@ -5444,3 +6843,35 @@ Run it from the top source directory using the command
   let close = output_to "ruby/ext/guestfs/_guestfs.c" in
   generate_ruby_c ();
   close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/GuestFS.java" in
+  generate_java_java ();
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/PV.java" in
+  generate_java_struct "PV" pv_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/VG.java" in
+  generate_java_struct "VG" vg_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/LV.java" in
+  generate_java_struct "LV" lv_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/Stat.java" in
+  generate_java_struct "Stat" stat_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/StatVFS.java" in
+  generate_java_struct "StatVFS" statvfs_cols;
+  close ();
+
+  let close = output_to "java/com_redhat_et_libguestfs_GuestFS.c" in
+  generate_java_c ();
+  close ();
+
+  let close = output_to "haskell/Guestfs.hs" in
+  generate_haskell_hs ();
+  close ();