[Libguestfs] [PATCH 4/4] qemu: Use sqlite to store qemu detection data.

Richard W.M. Jones rjones at redhat.com
Wed May 25 16:37:55 UTC 2016


Instead of saving qemu detection data in separate qemu.stat,
qemu.help, etc files, use a sqlite database for this data.

This adds quite a lot of code, but most of the complexity is because
we now handle concurrency correctly (the previous code probably
suffered from corruption bugs if multiple threads tried to update the
qemu.* files at the same time).

Also the use of a database allows us to do more advanced things:

 - We can store qemu information for multiple versions of qemu.

 - We can store the data in a structured way, instead of using
   "file blobs".

sqlite is fast.  On the hot path it only takes 0.4ms to open and read
the cached test results from the database.

This obviously adds sqlite3 as a new dependency of the library.
---
 .gitignore                |   1 +
 docs/guestfs-building.pod |   4 +
 m4/guestfs_libraries.m4   |   3 +
 src/Makefile.am           |  28 ++-
 src/launch-direct.c       |   3 +-
 src/qemu-schema.sql       |  70 ++++++
 src/qemu.c                | 626 +++++++++++++++++++++++++++++++++-------------
 7 files changed, 558 insertions(+), 177 deletions(-)
 create mode 100644 src/qemu-schema.sql

diff --git a/.gitignore b/.gitignore
index 285b28d..4b55a3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -465,6 +465,7 @@ Makefile.in
 /src/libguestfs.syms
 /src/.libs/libguestfs.so
 /src/libvirt-is-version
+/src/qemu-schema.c
 /src/stamp-guestfs.pod
 /src/structs-cleanup.c
 /src/structs-compare.c
diff --git a/docs/guestfs-building.pod b/docs/guestfs-building.pod
index f42d25f..3e48cc9 100644
--- a/docs/guestfs-building.pod
+++ b/docs/guestfs-building.pod
@@ -139,6 +139,10 @@ I<Required>.
 
 I<Required>.
 
+=item sqlite3
+
+I<Required>.
+
 =item genisoimage
 
 I<Required>.
diff --git a/m4/guestfs_libraries.m4 b/m4/guestfs_libraries.m4
index e845ea9..4b6db95 100644
--- a/m4/guestfs_libraries.m4
+++ b/m4/guestfs_libraries.m4
@@ -234,6 +234,9 @@ PKG_CHECK_MODULES([PCRE], [libpcre])
 dnl Check for Augeas >= 1.0.0 (required).
 PKG_CHECK_MODULES([AUGEAS],[augeas >= 1.0.0])
 
+dnl Check for sqlite3 (required)
+PKG_CHECK_MODULES([SQLITE], [sqlite3])
+
 dnl libmagic (highly recommended)
 AC_CHECK_LIB([magic],[magic_file],[
     AC_CHECK_HEADER([magic.h],[
diff --git a/src/Makefile.am b/src/Makefile.am
index d659f8d..3dc3592 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -19,7 +19,7 @@ include $(top_srcdir)/subdir-rules.mk
 
 noinst_PROGRAMS =
 
-CLEANFILES = stamp-guestfs.pod
+CLEANFILES = qemu-schema.c stamp-guestfs.pod
 
 generator_built = \
 	guestfs_protocol.x \
@@ -54,14 +54,16 @@ BUILT_SOURCES = \
 	$(generator_built) \
 	guestfs_protocol.c \
 	guestfs_protocol.h \
-	errnostring-gperf.c
+	errnostring-gperf.c \
+	qemu-schema.c
 
 EXTRA_DIST = \
 	$(BUILT_SOURCES) \
 	MAX_PROC_NR \
 	libguestfs.3 \
 	libguestfs.pc.in \
-	guestfs.pod
+	guestfs.pod \
+	qemu-schema.sql
 
 include_HEADERS = guestfs.h
 
@@ -126,6 +128,7 @@ libguestfs_la_SOURCES = \
 	private-data.c \
 	proto.c \
 	qemu.c \
+	qemu-schema.c \
 	stringsbuf.c \
 	structs-compare.c \
 	structs-copy.c \
@@ -148,6 +151,7 @@ libguestfs_la_CFLAGS = \
 	$(WARN_CFLAGS) $(WERROR_CFLAGS) \
 	$(GCC_VISIBILITY_HIDDEN) \
 	$(PCRE_CFLAGS) \
+	$(SQLITE_CFLAGS) \
 	$(LIBVIRT_CFLAGS) \
 	$(LIBXML2_CFLAGS) \
 	$(YAJL_CFLAGS)
@@ -157,6 +161,7 @@ libguestfs_la_LIBADD = \
 	libprotocol.la \
 	libutils.la \
 	$(PCRE_LIBS) $(MAGIC_LIBS) \
+	$(SQLITE_LIBS) \
 	$(LIBVIRT_LIBS) $(LIBXML2_LIBS) \
 	$(SELINUX_LIBS) \
 	$(YAJL_LIBS) \
@@ -228,6 +233,23 @@ guestfs_protocol.h: guestfs_protocol.x
 	mv $@-t $@
 endif
 
+# qemu-schema.c is built from qemu-schema.sql
+qemu-schema.c: qemu-schema.sql
+	rm -f $@-t $@
+	$(AWK) ' \
+	  BEGIN { \
+            OFS = ""; \
+	    print "/* Generated from qemu-schema.sql */"; \
+	    print "const char *guestfs_int_qemu_schema ="; \
+	  } \
+	  /^--/ { next } \
+          /^$$/ { next } \
+	  { print "\"", $$0, "\\n\"" } \
+	  END { print ";" } \
+	' < $< > $@-t
+	mv $@-t $@
+	chmod -w $@
+
 # libutils.la contains code outside libguestfs which is also
 # included in tools and bindings.
 libutils_la_SOURCES = \
diff --git a/src/launch-direct.c b/src/launch-direct.c
index 332118e..6bef940 100644
--- a/src/launch-direct.c
+++ b/src/launch-direct.c
@@ -579,8 +579,7 @@ launch_direct (guestfs_h *g, void *datav, const char *arg)
   ADD_CMDLINE ("stdio");
 
   if (g->verbose &&
-      guestfs_int_qemu_supports_device (g, data->qemu_data,
-                                        "Serial Graphics Adapter")) {
+      guestfs_int_qemu_supports_device (g, data->qemu_data, "sga")) {
     /* Use sgabios instead of vgabios.  This means we'll see BIOS
      * messages on the serial port, and also works around this bug
      * in qemu 1.1.0:
diff --git a/src/qemu-schema.sql b/src/qemu-schema.sql
new file mode 100644
index 0000000..1332b47
--- /dev/null
+++ b/src/qemu-schema.sql
@@ -0,0 +1,70 @@
+-- -*- mode: sql; sql-product: sqlite; -*-
+-- libguestfs
+-- Copyright (C) 2016 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
+
+-- This is the sqlite3 schema used to cache the qemu detection data.
+-- See qemu.c for further details.
+--
+-- Notes:
+--
+-- (1) If schema changes you MUST increment SCHEMA_GENERATION in
+-- qemu.c.  That's so we don't have to deal with updating an existing
+-- database to the new schema.
+--
+-- (2) ONLY use 'create table if not exists' or 'create unique index
+-- if not exists' commands.  That is so it is safe for multiple
+-- processes to run this SQL at the same time.  You must not use other
+-- statements such as inserts etc.
+
+-- The list of qemu binaries that we have detected.
+create table if not exists
+    qemu_binary (
+        id integer not null primary key,
+        size integer not null,      -- size of binary
+        mtime integer not null,     -- mtime of binary
+        path string not null,       -- path to binary (for info only)
+        help string not null,       -- qemu -help output
+        major integer not null,     -- qemu version
+        minor integer not null,
+        release integer not null
+    );
+
+create unique index if not exists
+    qemu_binary_size_mtime on qemu_binary (size, mtime);
+
+-- The 'qemu -device ?' output.
+--
+-- There may be multiple lines per qemu binary.
+create table if not exists
+    qemu_devices (
+        qemuid integer not null,
+        device string not null,
+        foreign key (qemuid) references qemu_binary (id)
+    );
+
+-- The 'qemu -L ?' output (datadirs).
+--
+-- There may be multiple lines per qemu binary, or no lines for
+-- old versions of qemu which did not support -L ?.  The lines are
+-- ordered by the id.
+create table if not exists
+    qemu_datadirs (
+        id integer not null primary key,
+        qemuid integer not null,
+        datadir string not null,
+        foreign key (qemuid) references qemu_binary (id)
+    );
diff --git a/src/qemu.c b/src/qemu.c
index f8ba5d2..a829869 100644
--- a/src/qemu.c
+++ b/src/qemu.c
@@ -39,39 +39,69 @@
 
 #include <pcre.h>
 
+#include <sqlite3.h>
+
 #include "ignore-value.h"
 
 #include "guestfs.h"
 #include "guestfs-internal.h"
 #include "guestfs_protocol.h"
 
+#ifdef HAVE_ATTRIBUTE_CLEANUP
+#define CLEANUP_SQLITE3_FREE __attribute__((cleanup(cleanup_sqlite3_free)))
+#define CLEANUP_SQLITE3_FINALIZE __attribute__((cleanup(cleanup_sqlite3_finalize)))
+
+static void
+cleanup_sqlite3_free (void *vpp)
+{
+  char *p = * (char **) vpp;
+  sqlite3_free (p);
+}
+
+static void
+cleanup_sqlite3_finalize (void *ptr)
+{
+  sqlite3_stmt *stmt = * (sqlite3_stmt **) ptr;
+  sqlite3_finalize (stmt);
+}
+
+#else /* !HAVE_ATTRIBUTE_CLEANUP */
+#define CLEANUP_SQLITE3_FREE
+#define CLEANUP_SQLITE3_FINALIZE
+#endif
+
 COMPILE_REGEXP (re_major_minor, "(\\d+)\\.(\\d+)", 0)
 
 struct qemu_data {
-  char *qemu_help;            /* Output of qemu -help. */
-  char *qemu_devices;         /* Output of qemu -device ? */
-  char *qemu_datadirs;        /* Output of qemu -L ? (NULL if not supported) */
-
-  int virtio_scsi;            /* See function
-                                 guestfs_int_qemu_supports_virtio_scsi */
+  sqlite3 *db;             /* Database storing qemu detection data. */
+  int64_t qemuid;          /* qemu_binary.id */
+  int virtio_scsi;         /* See function
+                              guestfs_int_qemu_supports_virtio_scsi */
 };
 
-static int test_qemu (guestfs_h *g, struct qemu_data *data, struct version *qemu_version);
+static int db_create_tables (guestfs_h *g, sqlite3 *db);
+static int64_t db_check_if_cached (guestfs_h *g, sqlite3 *db, const struct stat *statbuf);
+static int64_t db_insert_qemu_binary (guestfs_h *g, sqlite3 *db, const struct stat *statbuf, const char *qemu_path, const char *qemu_help, const struct version *qemu_version);
+static int db_select_qemu_version (guestfs_h *g, sqlite3 *db, int64_t qemuid, struct version *qemu_version);
+static void db_insert_devices_row (guestfs_h *g, void *vp, const char *buf, size_t len);
+static void db_insert_datadirs_row (guestfs_h *g, void *vp, const char *buf, size_t len);
+static int64_t probe_qemu_binary (guestfs_h *g, sqlite3 *db, const struct stat *statbuf);
 static void parse_qemu_version (guestfs_h *g, const char *, struct version *qemu_version);
 static void read_all (guestfs_h *g, void *retv, const char *buf, size_t len);
 
-/* This is saved in the qemu.stat file, so if we decide to change the
- * test_qemu memoization format/data in future, we should increment
- * this to discard any memoized data cached by previous versions of
- * libguestfs.
+/* This is saved in the database filename, so if we decide to change
+ * the schema in future, we should increment this to discard any
+ * memoized data cached by previous versions of libguestfs.
+ *
+ * NB: If the qemu-schema.sql changes you MUST increment
+ * SCHEMA_GENERATION.  That's so we don't have to deal with updating
+ * an existing database to the new schema.
  */
-#define MEMO_GENERATION 1
+#define SCHEMA_GENERATION 2
+extern const char *guestfs_int_qemu_schema;
 
 /**
- * Test qemu binary (or wrapper) runs, and do C<qemu -help> so we know
- * the version of qemu what options this qemu supports,
- * C<qemu -device ?> so we know what devices are available,
- * and C<qemu -L ?> to list data directories.
+ * Test qemu binary (or wrapper).
  *
  * The version number of qemu (from the C<-help> output) is saved in
  * C<&qemu_version>.
@@ -83,175 +113,354 @@ struct qemu_data *
 guestfs_int_test_qemu (guestfs_h *g, struct version *qemu_version)
 {
   struct qemu_data *data;
+  CLEANUP_FREE char *cachedir = NULL, *qemu_db_filename = NULL;
   struct stat statbuf;
-  CLEANUP_FREE char *cachedir = NULL, *qemu_stat_filename = NULL,
-    *qemu_help_filename = NULL, *qemu_devices_filename = NULL,
-    *qemu_datadirs_filename = NULL;
-  FILE *fp;
-  int generation;
-  uint64_t prev_size, prev_mtime;
-
-  if (stat (g->hv, &statbuf) == -1) {
-    perrorf (g, "stat: %s", g->hv);
-    return NULL;
-  }
+  sqlite3 *db;
+  int r;
+  int64_t qemuid;
+  CLEANUP_SQLITE3_FREE char *errmsg = NULL;
 
   cachedir = guestfs_int_lazy_make_supermin_appliance_dir (g);
   if (cachedir == NULL)
     return NULL;
 
-  qemu_stat_filename = safe_asprintf (g, "%s/qemu.stat", cachedir);
-  qemu_help_filename = safe_asprintf (g, "%s/qemu.help", cachedir);
-  qemu_devices_filename = safe_asprintf (g, "%s/qemu.devices", cachedir);
-  qemu_datadirs_filename = safe_asprintf (g, "%s/qemu.datadirs", cachedir);
+  qemu_db_filename = safe_asprintf (g, "%s/qemu.%d.db",
+                                    cachedir, SCHEMA_GENERATION);
+
+  /* Warn if sqlite wasn't compiled to be thread-safe. */
+  if (sqlite3_threadsafe () == 0)
+    warning (g,
+             _("sqlite3 is not thread safe, use of libguestfs"
+               " from multiple threads may cause data corruption."));
+
+  /* Open the database, or create it. */
+  r = sqlite3_open (qemu_db_filename, &db);
+  if (r != SQLITE_OK) {
+    error (g, "%s: %s", qemu_db_filename, sqlite3_errmsg (db));
+    return NULL;
+  }
 
-  /* Did we previously test the same version of qemu? */
-  debug (g, "checking for previously cached test results of %s, in %s",
-         g->hv, cachedir);
+  sqlite3_busy_timeout (db, 10000 /* ms */);
 
-  fp = fopen (qemu_stat_filename, "r");
-  if (fp == NULL)
-    goto do_test;
-  if (fscanf (fp, "%d %" SCNu64 " %" SCNu64,
-              &generation, &prev_size, &prev_mtime) != 3) {
-    fclose (fp);
-    goto do_test;
+  /* The create table statements from qemu-schema.sql are designed to
+   * be safe to run in parallel or to be run if the database has
+   * already been created.  So do those outside the transaction.
+   */
+  if (db_create_tables (g, db) == -1) {
+    sqlite3_close (db);
+    return NULL;
   }
-  fclose (fp);
 
-  if (generation == MEMO_GENERATION &&
-      (uint64_t) statbuf.st_size == prev_size &&
-      (uint64_t) statbuf.st_mtime == prev_mtime) {
-    /* Same binary as before, so read the previously cached qemu -help
-     * and qemu -devices ? output.
-     */
-    if (access (qemu_help_filename, R_OK) == -1 ||
-        access (qemu_devices_filename, R_OK) == -1 ||
-        access (qemu_datadirs_filename, R_OK) == -1)
-      goto do_test;
+  /* Find out if we've probed this version of qemu before.  Do this
+   * outside the transaction since it only involves a select stmt.
+   */
+  debug (g, "checking for previously cached test results of %s", g->hv);
 
-    debug (g, "loading previously cached test results");
+  if (stat (g->hv, &statbuf) == -1) {
+    perrorf (g, "stat: %s", g->hv);
+    sqlite3_close (db);
+    return NULL;
+  }
 
-    data = safe_calloc (g, 1, sizeof *data);
+  qemuid = db_check_if_cached (g, db, &statbuf);
+  if (qemuid == -1) {
+    sqlite3_close (db);
+    return NULL;
+  }
 
-    if (guestfs_int_read_whole_file (g, qemu_help_filename,
-                                     &data->qemu_help, NULL) == -1) {
-      guestfs_int_free_qemu_data (data);
+  if (qemuid > 0)
+    /* Previously probed this qemu.  This is the fast path. */
+    debug (g, "using previously cached test results");
+  else {
+    debug (g, "no previously cached results found, testing qemu binary");
+
+    /* A new version of qemu.  This is the slow path.  Begin an
+     * exclusive transaction while we probe qemu.  This will serialize
+     * all qemu probes, but not the later select statements where we
+     * retrieve the probed data.
+     */
+    r = sqlite3_exec (db, "begin exclusive transaction", NULL, 0, &errmsg);
+    if (r != SQLITE_OK) {
+      error (g, "sqlite3: begin transaction: %s", errmsg);
+      sqlite3_close (db);
       return NULL;
     }
 
-    parse_qemu_version (g, data->qemu_help, qemu_version);
-
-    if (guestfs_int_read_whole_file (g, qemu_devices_filename,
-                                     &data->qemu_devices, NULL) == -1) {
-      guestfs_int_free_qemu_data (data);
+    /* After acquiring the exclusive lock, it may be that another
+     * thread has probed this qemu binary and inserted the data.  If
+     * we go ahead and insert the same data then we'll get a unique
+     * constraint violation.  Check if that is the case, and skip
+     * probing if so.
+     */
+    qemuid = db_check_if_cached (g, db, &statbuf);
+    if (qemuid == -1)
+      goto rollback;
+    if (qemuid > 0)
+      goto skip_probing;
+
+    qemuid = probe_qemu_binary (g, db, &statbuf);
+    if (qemuid == -1) {
+    rollback:
+      /* This will rollback the transaction and release all locks. */
+      sqlite3_close (db);
       return NULL;
     }
 
-    if (guestfs_int_read_whole_file (g, qemu_datadirs_filename,
-                                     &data->qemu_datadirs, NULL) == -1) {
-      guestfs_int_free_qemu_data (data);
+  skip_probing:
+    /* Commit the probed data. */
+    r = sqlite3_exec (db, "commit transaction", NULL, 0, &errmsg);
+    if (r != SQLITE_OK) {
+      error (g, "sqlite3: commit transaction: %s", errmsg);
+      sqlite3_close (db);
       return NULL;
     }
 
-    return data;
-  }
-
- do_test:
-  data = safe_calloc (g, 1, sizeof *data);
-
-  if (test_qemu (g, data, qemu_version) == -1) {
-    guestfs_int_free_qemu_data (data);
-    return NULL;
-  }
-
-  /* Now memoize the qemu output in the cache directory. */
-  debug (g, "saving test results");
-
-  fp = fopen (qemu_help_filename, "w");
-  if (fp == NULL) {
-  help_error:
-    perrorf (g, "%s", qemu_help_filename);
-    if (fp != NULL) fclose (fp);
-    guestfs_int_free_qemu_data (data);
-    return NULL;
-  }
-  if (fprintf (fp, "%s", data->qemu_help) == -1)
-    goto help_error;
-  if (fclose (fp) == -1)
-    goto help_error;
-
-  fp = fopen (qemu_devices_filename, "w");
-  if (fp == NULL) {
-  devices_error:
-    perrorf (g, "%s", qemu_devices_filename);
-    if (fp != NULL) fclose (fp);
-    guestfs_int_free_qemu_data (data);
-    return NULL;
-  }
-  if (fprintf (fp, "%s", data->qemu_devices) == -1)
-    goto devices_error;
-  if (fclose (fp) == -1)
-    goto devices_error;
-
-  fp = fopen (qemu_datadirs_filename, "w");
-  if (fp == NULL) {
-  datadirs_error:
-    perrorf (g, "%s", qemu_datadirs_filename);
-    if (fp != NULL) fclose (fp);
-    guestfs_int_free_qemu_data (data);
-    return NULL;
+    /* After this point, any select statements to retrieve the data will
+     * use autocommit.
+     */
   }
-  if (fprintf (fp, "%s", data->qemu_datadirs) == -1)
-    goto datadirs_error;
-  if (fclose (fp) == -1)
-    goto datadirs_error;
 
-  /* Write the qemu.stat file last so that its presence indicates that
-   * the qemu.help and qemu.devices files ought to exist.
+  /* Get the version of qemu from the database and return it to the
+   * caller via the qemu_version struct.
    */
-  fp = fopen (qemu_stat_filename, "w");
-  if (fp == NULL) {
-  stat_error:
-    perrorf (g, "%s", qemu_stat_filename);
-    if (fp != NULL) fclose (fp);
-    guestfs_int_free_qemu_data (data);
+  if (db_select_qemu_version (g, db, qemuid, qemu_version) == -1) {
+    sqlite3_close (db);
     return NULL;
   }
-  /* The path to qemu is stored for information only, it is not
-   * used when we parse the file.
-   */
-  if (fprintf (fp, "%d %" PRIu64 " %" PRIu64 " %s\n",
-               MEMO_GENERATION,
-               (uint64_t) statbuf.st_size,
-               (uint64_t) statbuf.st_mtime,
-               g->hv) == -1)
-    goto stat_error;
-  if (fclose (fp) == -1)
-    goto stat_error;
 
+  data = safe_malloc (g, sizeof *data);
+  data->qemuid = qemuid;
+  data->db = db;
+  data->virtio_scsi = 0;
   return data;
 }
 
+/**
+ * Create the database schema.  We use "create table if not exists" so
+ * that these statements have no effect if we're reopening an existing
+ * database file.
+ */
 static int
-test_qemu (guestfs_h *g, struct qemu_data *data, struct version *qemu_version)
+db_create_tables (guestfs_h *g, sqlite3 *db)
+{
+  int r;
+  CLEANUP_SQLITE3_FREE char *errmsg = NULL;
+
+  r = sqlite3_exec (db, guestfs_int_qemu_schema, NULL, 0, &errmsg);
+  if (r != SQLITE_OK) {
+    error (g, "sqlite3: error creating database schema: %s", errmsg);
+    return -1;
+  }
+
+  return 0;
+}
+
+/**
+ * Check for a previously cached qemu result in the database.
+ *
+ * Returns qemu ID E<ge> 1.  Returns C<0> if not found.  Returns
+ * C<-1> on error.
+ */
+static int64_t
+db_check_if_cached (guestfs_h *g, sqlite3 *db, const struct stat *statbuf)
+{
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
+  int64_t qemuid;
+
+  r = sqlite3_prepare_v2
+    (db,
+     "select id from qemu_binary where size = ? and mtime = ?",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    error (g, "sqlite3: prepare: %s", sqlite3_errmsg (db));
+    return -1;
+  }
+  sqlite3_bind_int64 (stmt, 1, (sqlite3_int64) statbuf->st_size);
+  sqlite3_bind_int64 (stmt, 2, (sqlite3_int64) statbuf->st_mtime);
+
+  r = sqlite3_step (stmt);
+  if (r == SQLITE_DONE)         /* not found */
+    return 0;
+
+  if (r != SQLITE_ROW) {
+    error (g, "sqlite3: step: %s", sqlite3_errmsg (db));
+    return -1;
+  }
+
+  /* found 1 row */
+  qemuid = sqlite3_column_int64 (stmt, 0);
+  assert (qemuid > 0);
+  return qemuid;
+}
+
+/**
+ * Insert a row in the qemu_binary table recording that we have
+ * cached data for this binary.  Returns the qemuid E<ge> 1, or
+ * C<-1> on error.
+ */
+static int64_t
+db_insert_qemu_binary (guestfs_h *g, sqlite3 *db,
+                       const struct stat *statbuf,
+                       const char *qemu_path,
+                       const char *qemu_help,
+                       const struct version *qemu_version)
+{
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
+  int64_t qemuid;
+
+  r = sqlite3_prepare_v2
+    (db,
+     "insert into qemu_binary (id, size, mtime, path, help,"
+     "                         major, minor, release)"
+     " values (NULL, ?, ?, ?, ?, ?, ?, ?)",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    error (g, "sqlite3: prepare: %s", sqlite3_errmsg (db));
+    return -1;
+  }
+  sqlite3_bind_int64 (stmt, 1, (sqlite3_int64) statbuf->st_size);
+  sqlite3_bind_int64 (stmt, 2, (sqlite3_int64) statbuf->st_mtime);
+  sqlite3_bind_text (stmt, 3, qemu_path, -1, SQLITE_TRANSIENT);
+  sqlite3_bind_text (stmt, 4, qemu_help, -1, SQLITE_TRANSIENT);
+  sqlite3_bind_int (stmt, 5, qemu_version->v_major);
+  sqlite3_bind_int (stmt, 6, qemu_version->v_minor);
+  sqlite3_bind_int (stmt, 7, qemu_version->v_micro);
+
+  r = sqlite3_step (stmt);
+  if (r != SQLITE_DONE) {
+    error (g, "sqlite3: insert: %s", sqlite3_errmsg (db));
+    return -1;
+  }
+
+  qemuid = sqlite3_last_insert_rowid (db);
+  assert (qemuid > 0);
+  return qemuid;
+}
+
+/**
+ * Read the qemu version from the database.
+ */
+static int
+db_select_qemu_version (guestfs_h *g, sqlite3 *db,
+                        int64_t qemuid, struct version *qemu_version)
+{
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
+
+  r = sqlite3_prepare_v2
+    (db,
+     "select major, minor, release from qemu_binary where id = ?",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    error (g, "sqlite3: prepare: %s", sqlite3_errmsg (db));
+    return -1;
+  }
+  sqlite3_bind_int64 (stmt, 1, qemuid);
+
+  r = sqlite3_step (stmt);
+  if (r != SQLITE_ROW) {
+    error (g, "sqlite3: step: %s", sqlite3_errmsg (db));
+    return -1;
+  }
+
+  guestfs_int_version_from_values (qemu_version,
+                                   sqlite3_column_int (stmt, 0),
+                                   sqlite3_column_int (stmt, 1),
+                                   sqlite3_column_int (stmt, 2));
+  return 0;
+}
+
+struct db_qemuid {
+  sqlite3 *db;
+  unsigned db_errors;
+  int64_t qemuid;
+};
+
+static void
+db_insert_devices_row (guestfs_h *g, void *vp, const char *buf, size_t len)
+{
+  struct db_qemuid *db_qemuid = vp;
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
+
+  r = sqlite3_prepare_v2
+    (db_qemuid->db,
+     "insert into qemu_devices (qemuid, device) values (?, ?)",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    db_qemuid->db_errors++;
+    return;
+  }
+  sqlite3_bind_int64 (stmt, 1, db_qemuid->qemuid);
+  sqlite3_bind_text (stmt, 2, buf, len, SQLITE_TRANSIENT);
+
+  r = sqlite3_step (stmt);
+  if (r != SQLITE_DONE)
+    db_qemuid->db_errors++;
+}
+
+static void
+db_insert_datadirs_row (guestfs_h *g, void *vp, const char *buf, size_t len)
+{
+  struct db_qemuid *db_qemuid = vp;
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
+
+  r = sqlite3_prepare_v2
+    (db_qemuid->db,
+     "insert into qemu_datadirs (id, qemuid, datadir) values (NULL, ?, ?)",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    db_qemuid->db_errors++;
+    return;
+  }
+  sqlite3_bind_int64 (stmt, 1, db_qemuid->qemuid);
+  sqlite3_bind_text (stmt, 2, buf, len, SQLITE_TRANSIENT);
+
+  r = sqlite3_step (stmt);
+  if (r != SQLITE_DONE)
+    db_qemuid->db_errors++;
+}
+
+static int64_t
+probe_qemu_binary (guestfs_h *g, sqlite3 *db, const struct stat *statbuf)
 {
   CLEANUP_CMD_CLOSE struct command *cmd1 = guestfs_int_new_command (g);
   CLEANUP_CMD_CLOSE struct command *cmd2 = guestfs_int_new_command (g);
   CLEANUP_CMD_CLOSE struct command *cmd3 = guestfs_int_new_command (g);
+  CLEANUP_FREE char *qemu_help = NULL;
   int r;
+  int64_t qemuid;
+  struct db_qemuid db_qemuid;
+  struct version qemu_version;
 
   guestfs_int_cmd_add_arg (cmd1, g->hv);
   guestfs_int_cmd_add_arg (cmd1, "-display");
   guestfs_int_cmd_add_arg (cmd1, "none");
   guestfs_int_cmd_add_arg (cmd1, "-help");
-  guestfs_int_cmd_set_stdout_callback (cmd1, read_all, &data->qemu_help,
+  guestfs_int_cmd_set_stdout_callback (cmd1, read_all, &qemu_help,
 				       CMD_STDOUT_FLAG_WHOLE_BUFFER);
   r = guestfs_int_cmd_run (cmd1);
-  if (r == -1 || !WIFEXITED (r) || WEXITSTATUS (r) != 0)
-    goto error;
+  if (r == -1 || !WIFEXITED (r) || WEXITSTATUS (r) != 0) {
+  cmd_error:
+    if (r == -1)
+      return -1;
+    guestfs_int_external_command_failed (g, r, g->hv, NULL);
+    return -1;
+  }
 
-  parse_qemu_version (g, data->qemu_help, qemu_version);
+  parse_qemu_version (g, qemu_help, &qemu_version);
+
+  qemuid = db_insert_qemu_binary (g, db, statbuf, g->hv,
+                                  qemu_help, &qemu_version);
+  if (qemuid == -1)
+    return -1;
+
+  db_qemuid.db = db;
+  db_qemuid.db_errors = 0;
+  db_qemuid.qemuid = qemuid;
 
   guestfs_int_cmd_add_arg (cmd2, g->hv);
   guestfs_int_cmd_add_arg (cmd2, "-display");
@@ -266,14 +475,18 @@ test_qemu (guestfs_h *g, struct qemu_data *data, struct version *qemu_version)
   guestfs_int_cmd_add_arg (cmd2, "?");
   guestfs_int_cmd_clear_capture_errors (cmd2);
   guestfs_int_cmd_set_stderr_to_stdout (cmd2);
-  guestfs_int_cmd_set_stdout_callback (cmd2, read_all, &data->qemu_devices,
-				       CMD_STDOUT_FLAG_WHOLE_BUFFER);
+  guestfs_int_cmd_set_stdout_callback (cmd2, db_insert_devices_row,
+                                       &db_qemuid, 0);
   r = guestfs_int_cmd_run (cmd2);
   if (r == -1 || !WIFEXITED (r) || WEXITSTATUS (r) != 0)
-    goto error;
+    goto cmd_error;
+  if (db_qemuid.db_errors != 0) {
+    error (g, "sqlite3: insert: %s", sqlite3_errmsg (db));
+    return -1;
+  }
 
   /* qemu -L ? only supported in qemu >= 2.7 */
-  if (guestfs_int_version_ge (qemu_version, 2, 6 /* XXX 7 */, 0)) {
+  if (guestfs_int_version_ge (&qemu_version, 2, 6 /* XXX 7 */, 0)) {
     guestfs_int_cmd_add_arg (cmd3, g->hv);
     guestfs_int_cmd_add_arg (cmd3, "-display");
     guestfs_int_cmd_add_arg (cmd3, "none");
@@ -285,23 +498,18 @@ test_qemu (guestfs_h *g, struct qemu_data *data, struct version *qemu_version)
                              "accel=kvm:tcg");
     guestfs_int_cmd_add_arg (cmd3, "-L");
     guestfs_int_cmd_add_arg (cmd3, "?");
-    guestfs_int_cmd_set_stdout_callback (cmd3, read_all, &data->qemu_datadirs,
-                                         CMD_STDOUT_FLAG_WHOLE_BUFFER);
+    guestfs_int_cmd_set_stdout_callback (cmd3, db_insert_datadirs_row,
+                                         &db_qemuid, 0);
     r = guestfs_int_cmd_run (cmd3);
     if (r == -1 || !WIFEXITED (r) || WEXITSTATUS (r) != 0)
-      goto error;
+      goto cmd_error;
+    if (db_qemuid.db_errors != 0) {
+      error (g, "sqlite3: insert: %s", sqlite3_errmsg (db));
+      return -1;
+    }
   }
-  else
-    data->qemu_datadirs = safe_strdup (g, "");
 
-  return 0;
-
- error:
-  if (r == -1)
-    return -1;
-
-  guestfs_int_external_command_failed (g, r, g->hv, NULL);
-  return -1;
+  return qemuid;
 }
 
 /**
@@ -353,19 +561,77 @@ int
 guestfs_int_qemu_supports (guestfs_h *g, const struct qemu_data *data,
                            const char *option)
 {
-  return strstr (data->qemu_help, option) != NULL;
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
+
+  assert (data->db != NULL);
+  assert (data->qemuid > 0);
+  assert (strchr (option, '%') == NULL); /* SQL '%' is wildcard */
+
+  r = sqlite3_prepare_v2
+    (data->db,
+     "select 1 from qemu_binary where id = ? and help like '%' || ? || '%'",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    error (g, "sqlite3: prepare: %s", sqlite3_errmsg (data->db));
+    return -1;
+  }
+  sqlite3_bind_int64 (stmt, 1, data->qemuid);
+  sqlite3_bind_text (stmt, 2, option, -1, SQLITE_TRANSIENT);
+
+  r = sqlite3_step (stmt);
+  if (r == SQLITE_DONE)         /* not found */
+    return 0;
+
+  if (r != SQLITE_ROW) {
+    error (g, "sqlite3: step: %s", sqlite3_errmsg (data->db));
+    return -1;
+  }
+
+  return 1;
 }
 
 /**
- * Test if device is supported by qemu (currently just greps the
- * C<qemu -device ?> output).
+ * Test if device is supported by qemu.
  */
 int
 guestfs_int_qemu_supports_device (guestfs_h *g,
                                   const struct qemu_data *data,
                                   const char *device_name)
 {
-  return strstr (data->qemu_devices, device_name) != NULL;
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
+
+  assert (data->db != NULL);
+  assert (data->qemuid > 0);
+  assert (strchr (device_name, '%') == NULL); /* SQL '%' is wildcard */
+
+  /* Each line from qemu -device ? looks like:
+   * name "i6300esb", bus PCI
+   */
+  r = sqlite3_prepare_v2
+    (data->db,
+     "select 1 from qemu_devices"
+     " where qemuid = ?"
+     "   and device like 'name \"' || ? || '\", %'",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    error (g, "sqlite3: prepare: %s", sqlite3_errmsg (data->db));
+    return -1;
+  }
+  sqlite3_bind_int64 (stmt, 1, data->qemuid);
+  sqlite3_bind_text (stmt, 2, device_name, -1, SQLITE_TRANSIENT);
+
+  r = sqlite3_step (stmt);
+  if (r == SQLITE_DONE)         /* not found */
+    return 0;
+
+  if (r != SQLITE_ROW) {
+    error (g, "sqlite3: step: %s", sqlite3_errmsg (data->db));
+    return -1;
+  }
+
+  return 1;
 }
 
 /**
@@ -376,22 +642,40 @@ guestfs_int_qemu_supports_bios (guestfs_h *g,
                                 const struct qemu_data *data,
                                 const char *bios_name)
 {
-  CLEANUP_FREE_STRING_LIST char **datadirs;
-  size_t i;
+  CLEANUP_SQLITE3_FINALIZE sqlite3_stmt *stmt = NULL;
+  int r;
 
-  datadirs = guestfs_int_split_string ('\n', data->qemu_datadirs);
-  if (datadirs == NULL)
-    return 0;          /* ignore errors, return false which is safe */
+  assert (data->db != NULL);
+  assert (data->qemuid > 0);
+  assert (strchr (bios_name, '%') == NULL); /* SQL '%' is wildcard */
 
-  for (i = 0; datadirs[i] != NULL; ++i) {
+  r = sqlite3_prepare_v2
+    (data->db,
+     "select datadir from qemu_datadirs"
+     " where qemuid = ?"
+     " order by id",
+     -1, &stmt, NULL);
+  if (r != SQLITE_OK) {
+    error (g, "sqlite3: prepare: %s", sqlite3_errmsg (data->db));
+    return -1;
+  }
+  sqlite3_bind_int64 (stmt, 1, data->qemuid);
+
+  while ((r = sqlite3_step (stmt)) == SQLITE_ROW) {
+    const char *datadir = (const char *) sqlite3_column_text (stmt, 0);
     CLEANUP_FREE char *path;
 
-    path = safe_asprintf (g, "%s/%s", datadirs[i], bios_name);
+    path = safe_asprintf (g, "%s/%s", datadir, bios_name);
     if (access (path, R_OK) == 0)
       return 1;
   }
 
-  return 0;
+  if (r != SQLITE_DONE) {
+    error (g, "sqlite3: step: %s", sqlite3_errmsg (data->db));
+    return -1;
+  }
+
+  return 0;                     /* BIOS not found. */
 }
 
 static int
@@ -777,9 +1061,7 @@ void
 guestfs_int_free_qemu_data (struct qemu_data *data)
 {
   if (data) {
-    free (data->qemu_help);
-    free (data->qemu_devices);
-    free (data->qemu_datadirs);
+    sqlite3_close (data->db);
     free (data);
   }
 }
-- 
2.7.4




More information about the Libguestfs mailing list