[git] GPGME - branch, master, updated. gpgme-1.4.3-21-g4f2d652

by Werner Koch cvs at cvs.gnupg.org
Thu Apr 10 13:04:59 CEST 2014


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "GnuPG Made Easy".

The branch, master has been updated
       via  4f2d652e60700e03809307a10015ff9003ac3579 (commit)
       via  77931a9a1468b0cfbaafd4153867d90764a6d258 (commit)
       via  b788c36ec05f73da3ca71aab337432adb203ffd9 (commit)
       via  443f6b76a964b77e2e61c03592a83ffa0a6da4b1 (commit)
       via  617d3be629229cbebfdc2d26a4e854bc4fe38a68 (commit)
      from  766355b5d4f63261c428531fdbdafb8b67de9369 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 4f2d652e60700e03809307a10015ff9003ac3579
Author: Werner Koch <wk at gnupg.org>
Date:   Thu Apr 10 13:01:00 2014 +0200

    Add GPGME_PROTOCOL_SPAWN and gpgme_op_spawn.
    
    * src/gpgme.h.in (GPGME_PROTOCOL_SPAWN): New.
    (GPGME_SPAWN_DETACHED, GPGME_SPAWN_ALLOW_SET_FG): New.
    * src/gpgme.c (gpgme_set_protocol): Add new protocol.
    (gpgme_get_protocol_name): Ditto.
    * src/spawn.c: New.
    * src/libgpgme.vers, src/gpgme.def: Add new public functions.
    * src/engine-spawn.c: New.
    * src/Makefile.am: Add new files.
    * src/engine-backend.h (struct engine_ops): Add OPSPAWN.
    * src/engine.c (engine_ops): Add _gpgme_engine_ops_spawn.
    (gpgme_get_engine_info): Add Spawn to the list of protocols.
    (_gpgme_engine_op_spawn): New.
    
    * src/gpgme-tool.c (gt_protocol_from_name): Add new protocol.
    (gt_spawn, cmd_spawn): New.

diff --git a/NEWS b/NEWS
index f625ab9..acba24d 100644
--- a/NEWS
+++ b/NEWS
@@ -9,9 +9,17 @@ Noteworthy changes in version 1.5.0 (unreleased)
    whatever gpgconf tells as name for the OpenPGP engine.  If gpgconf
    is not found, GPGME looks for an engine named "gpg".
 
+ * Add feature to use the gpgme I/O subsystem to run arbitrary
+   commands.
+
  * Interface changes relative to the 1.4.3 release:
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  gpgme_get_dirinfo              NEW.
+ gpgme_op_spawn_start           NEW.
+ gpgme_op_spawn                 NEW.
+ GPGME_PROTOCOL_SPAWN           NEW.
+ GPGME_SPAWN_DETACHED           NEW.
+ GPGME_SPAWN_ALLOW_SET_FG       NEW.
 
 
 Noteworthy changes in version 1.4.3 (2013-08-12)
diff --git a/doc/gpgme.texi b/doc/gpgme.texi
index e12fd73..027e1ef 100644
--- a/doc/gpgme.texi
+++ b/doc/gpgme.texi
@@ -113,7 +113,6 @@ Indices
 * Concept Index::                 Index of concepts and programs.
 * Function and Data Index::       Index of functions, variables and data types.
 
-
 @detailmenu
  --- The Detailed Node Listing ---
 
@@ -170,6 +169,7 @@ Manipulating Data Buffers
 
 * Data Buffer I/O Operations::    I/O operations on data buffers.
 * Data Buffer Meta-Data::         Meta-data manipulation of data buffers.
+* Data Buffer Convenience::       Convenience function for data buffers.
 
 Contexts
 
@@ -180,6 +180,7 @@ Contexts
 * Key Management::                Managing keys with @acronym{GPGME}.
 * Trust Item Management::         Managing trust items with @acronym{GPGME}.
 * Crypto Operations::             Using a context for cryptography.
+* Miscellaneous::                 Miscellaneous operations.
 * Run Control::                   Controlling how operations are run.
 
 Context Attributes
@@ -204,6 +205,7 @@ Key Management
 * Exporting Keys::                Retrieving key data from the key ring.
 * Importing Keys::                Adding keys to the key ring.
 * Deleting Keys::                 Removing keys from the key ring.
+* Changing Passphrases::          Change the passphrase of a key.
 * Advanced Key Editing::          Advanced key edit operation.
 
 Trust Item Management
@@ -230,6 +232,10 @@ Encrypt
 
 * Encrypting a Plaintext::        How to encrypt a plaintext.
 
+Miscellaneous
+
+* Running other Programs::        Running other Programs
+
 Run Control
 
 * Waiting For Completion::        Waiting until an operation is completed.
@@ -850,6 +856,9 @@ Under development.  Please ask on @email{gnupg-devel@@gnupg.org} for help.
 @item GPGME_PROTOCOL_UISERVER
 Under development.  Please ask on @email{gnupg-devel@@gnupg.org} for help.
 
+ at item GPGME_PROTOCOL_SPAWN
+Special protocol for use with @code{gpgme_op_spawn}.
+
 @item GPGME_PROTOCOL_UNKNOWN
 Reserved for future extension.  You may use this to indicate that the
 used protocol is not known to the application.  Currently,
@@ -1946,7 +1955,7 @@ be used to manipulate both.
 @menu
 * Data Buffer I/O Operations::    I/O operations on data buffers.
 * Data Buffer Meta-Data::         Meta-data manipulation of data buffers.
-* Data Buffer Convenience::       Convenience fucntion for data buffers.
+* Data Buffer Convenience::       Convenience function for data buffers.
 @end menu
 
 
@@ -2187,6 +2196,7 @@ cryptographic operations.
 * Key Management::                Managing keys with @acronym{GPGME}.
 * Trust Item Management::         Managing trust items with @acronym{GPGME}.
 * Crypto Operations::             Using a context for cryptography.
+* Miscellaneous::                 Miscellaneous operations
 * Run Control::                   Controlling how operations are run.
 @end menu
 
@@ -2261,7 +2271,7 @@ started.  In fact, these references are accessed through the
 * Crypto Engine::                 Configuring the crypto engine.
 * ASCII Armor::                   Requesting @acronym{ASCII} armored output.
 * Text Mode::                     Choosing canonical text mode.
-* Included Certificates::       Including a number of certificates.
+* Included Certificates::         Including a number of certificates.
 * Key Listing Mode::              Selecting key listing mode.
 * Passphrase Callback::           Getting the passphrase from the user.
 * Progress Meter Callback::       Being informed about the progress.
@@ -5222,6 +5232,66 @@ pointer.
 @end deftypefun
 
 
+ at node Miscellaneous
+ at section Miscellaneous operations
+
+Here are some support functions which are sometimes useful.
+
+ at menu
+* Running other Programs::      Running other Programs
+ at end menu
+
+
+ at node Running other Programs
+ at subsection Running other Programs
+
+GPGME features an internal subsystem to run the actual backend
+engines.  Along with data abstraction object this subsystem can be
+used to run arbitrary simple programs which even need not be related
+to cryptographic features.  It may for example be used to run tools
+which are part of the GnuPG system but are not directly accessible
+with the GPGME API.
+
+
+ at deftypefun gpgme_error_t gpgme_op_spawn
+            (@w{gpgme_ctx_t @var{ctx}}, @w{const char *@var{file}}, @
+             @w{const char *@var{argv}[]}, @w{gpgme_data_t @var{datain}}, @
+             @w{gpgme_data_t @var{dataout}}, @w{gpgme_data_t @var{dataerr}}, @
+             @w{unsigned int @var{flags}})
+
+The function @code{gpgme_op_spawn} runs the program @var{file} with
+the arguments taken from the NULL terminated array @var{argv}.  If no
+arguments are required @var{argv} may be given as @code{NULL} (in that
+case GPGME uses the basename of @var{file} for @code{argv[0]}).  The
+file descriptors @code{stdin}, @code{stdout}, and @code{stderr} are
+connected to the data objects @var{datain}, @var{dataout}, and
+ at var{dataerr}.  If NULL is passed for one of these data objects the
+corresponding file descriptor is connected to @file{/dev/null}.
+
+The value in @var{flags} is a bitwise-or combination of one or
+multiple of the following bit values:
+
+ at table @code
+ at item GPGME_SPAWN_DETACHED
+Under Windows this flag inhibits the allocation of a new console for
+the program.  This is useful for a GUI application which needs to call
+a command line helper tool.
+ at item GPGME_SPAWN_ALLOW_SET_FG
+Under Windows this flag allows the called program to put itself into
+the foreground.
+ at end table
+ at end deftypefun
+
+ at deftypefun gpgme_error_t gpgme_op_spawn_start
+            (@w{gpgme_ctx_t @var{ctx}}, @w{const char *@var{file}}, @
+             @w{const char *@var{argv}[]}, @w{gpgme_data_t @var{datain}}, @
+             @w{gpgme_data_t @var{dataout}}, @w{gpgme_data_t @var{dataerr}}, @
+             @w{unsigned int @var{flags}})
+
+This is the asynchronous variant of @code{gpgme_op_spawn}.
+ at end deftypefun
+
+
 @node Run Control
 @section Run Control
 @cindex run control
diff --git a/src/Makefile.am b/src/Makefile.am
index abc014c..82f5327 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -89,11 +89,12 @@ main_sources =								\
 	sign.c passphrase.c progress.c					\
 	key.c keylist.c trust-item.c trustlist.c			\
 	import.c export.c genkey.c delete.c edit.c getauditlog.c        \
-	opassuan.c passwd.c assuan-support.c                            \
+	opassuan.c passwd.c spawn.c assuan-support.c                    \
 	engine.h engine-backend.h engine.c engine-gpg.c status-table.c	\
 	engine-gpgsm.c engine-assuan.c engine-gpgconf.c                 \
 	$(uiserver_components)						\
 	engine-g13.c vfs-mount.c vfs-create.c			        \
+	engine-spawn.c 	                                                \
 	gpgconf.c							\
 	sema.h priv-io.h $(system_components) sys-util.h dirinfo.c	\
 	debug.c debug.h gpgme.c version.c error.c
diff --git a/src/engine-assuan.c b/src/engine-assuan.c
index 5ef3047..663b2ea 100644
--- a/src/engine-assuan.c
+++ b/src/engine-assuan.c
@@ -783,5 +783,6 @@ struct engine_ops _gpgme_engine_ops_assuan =
     llass_cancel,
     llass_cancel_op,
     NULL,               /* passwd */
-    NULL                /* set_pinentry_mode */
+    NULL,               /* set_pinentry_mode */
+    NULL                /* opspawn */
   };
diff --git a/src/engine-backend.h b/src/engine-backend.h
index 7e6c0c1..dbb9e93 100644
--- a/src/engine-backend.h
+++ b/src/engine-backend.h
@@ -124,6 +124,14 @@ struct engine_ops
 
   /* Set the pinentry mode.  */
   gpgme_error_t (*set_pinentry_mode) (void *engine, gpgme_pinentry_mode_t mode);
+
+  /* The spawn command.  */
+  gpgme_error_t (*opspawn) (void * engine,
+                            const char *file, const char *argv[],
+                            gpgme_data_t datain,
+                            gpgme_data_t dataout,
+                            gpgme_data_t dataerr);
+
 };
 
 
@@ -135,6 +143,7 @@ extern struct engine_ops _gpgme_engine_ops_g13;         /* Crypto VFS. */
 #ifdef ENABLE_UISERVER
 extern struct engine_ops _gpgme_engine_ops_uiserver;
 #endif
+extern struct engine_ops _gpgme_engine_ops_spawn;       /* Spawn engine. */
 
 
 /* Prototypes for extra functions in engine-gpgconf.c  */
diff --git a/src/engine-g13.c b/src/engine-g13.c
index 75154ca..a9717ee 100644
--- a/src/engine-g13.c
+++ b/src/engine-g13.c
@@ -799,5 +799,6 @@ struct engine_ops _gpgme_engine_ops_g13 =
     g13_cancel,
     g13_cancel_op,
     NULL,               /* passwd */
-    NULL                /* set_pinentry_mode */
+    NULL,               /* set_pinentry_mode */
+    NULL                /* opspawn */
   };
diff --git a/src/engine-gpg.c b/src/engine-gpg.c
index 9037dd7..a8eab3a 100644
--- a/src/engine-gpg.c
+++ b/src/engine-gpg.c
@@ -2445,5 +2445,6 @@ struct engine_ops _gpgme_engine_ops_gpg =
     gpg_cancel,
     NULL,		/* cancel_op */
     gpg_passwd,
-    gpg_set_pinentry_mode
+    gpg_set_pinentry_mode,
+    NULL                /* opspawn */
   };
diff --git a/src/engine-gpgconf.c b/src/engine-gpgconf.c
index 811ad9d..a2407ac 100644
--- a/src/engine-gpgconf.c
+++ b/src/engine-gpgconf.c
@@ -963,5 +963,6 @@ struct engine_ops _gpgme_engine_ops_gpgconf =
     NULL,		/* cancel */
     NULL,               /* cancel_op */
     NULL,               /* passwd */
-    NULL                /* set_pinentry_mode */
+    NULL,               /* set_pinentry_mode */
+    NULL                /* opspawn */
   };
diff --git a/src/engine-gpgsm.c b/src/engine-gpgsm.c
index 6bcc095..710bf14 100644
--- a/src/engine-gpgsm.c
+++ b/src/engine-gpgsm.c
@@ -1988,5 +1988,6 @@ struct engine_ops _gpgme_engine_ops_gpgsm =
     gpgsm_cancel,
     NULL,		/* cancel_op */
     gpgsm_passwd,
-    NULL                /* set_pinentry_mode */
+    NULL,               /* set_pinentry_mode */
+    NULL                /* opspawn */
   };
diff --git a/src/engine-spawn.c b/src/engine-spawn.c
new file mode 100644
index 0000000..1e71c1c
--- /dev/null
+++ b/src/engine-spawn.c
@@ -0,0 +1,467 @@
+/* engine-spawn.c - Run an arbitrary program
+   Copyright (C) 2014 g10 Code GmbH
+
+   This file is part of GPGME.
+
+   GPGME 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.1 of
+   the License, or (at your option) any later version.
+
+   GPGME 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 program; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif
+#ifdef HAVE_LOCALE_H
+#include <locale.h>
+#endif
+
+#include "gpgme.h"
+#include "util.h"
+#include "ops.h"
+#include "wait.h"
+#include "context.h"  /*temp hack until we have GpmeData methods to do I/O */
+#include "priv-io.h"
+#include "sema.h"
+#include "debug.h"
+
+#include "engine-backend.h"
+
+
+/* This type is used to build a list of data sources/sinks.  */
+struct datalist_s
+{
+  struct datalist_s *next;
+  gpgme_data_t data;  /* The data object. */
+  int inbound;        /* True if this is used for reading from the peer.  */
+  int dup_to;         /* The fd used by the peer.  */
+};
+
+
+struct fd_data_map_s
+{
+  gpgme_data_t data;
+  int inbound;  /* True if this is used for reading from the peer. */
+  int dup_to;   /* Dup the fd to that one.  */
+  int fd;       /* The fd to use.  */
+  int peer_fd;  /* The other side of the pipe. */
+  void *tag;    /* Tag used by the I/O callback.  */
+};
+
+
+struct engine_spawn
+{
+  struct datalist_s *arglist;
+  struct datalist_s **argtail;
+
+  struct fd_data_map_s *fd_data_map;
+
+  struct gpgme_io_cbs io_cbs;
+};
+typedef struct engine_spawn *engine_spawn_t;
+
+
+static void engspawn_io_event (void *engine,
+                               gpgme_event_io_t type, void *type_data);
+static gpgme_error_t engspawn_cancel (void *engine);
+
+
+

+static void
+close_notify_handler (int fd, void *opaque)
+{
+  engine_spawn_t esp = opaque;
+  int i;
+
+  assert (fd != -1);
+
+  if (esp->fd_data_map)
+    {
+      for (i = 0; esp->fd_data_map[i].data; i++)
+	{
+	  if (esp->fd_data_map[i].fd == fd)
+	    {
+	      if (esp->fd_data_map[i].tag)
+		(*esp->io_cbs.remove) (esp->fd_data_map[i].tag);
+	      esp->fd_data_map[i].fd = -1;
+	      break;
+            }
+	  if (esp->fd_data_map[i].peer_fd == fd)
+	    {
+	      esp->fd_data_map[i].peer_fd = -1;
+	      break;
+            }
+        }
+    }
+}
+
+
+static gpgme_error_t
+add_data (engine_spawn_t esp, gpgme_data_t data, int dup_to, int inbound)
+{
+  struct datalist_s *a;
+
+  assert (esp);
+  assert (data);
+
+  a = malloc (sizeof *a - 1);
+  if (!a)
+    return gpg_error_from_syserror ();
+  a->next = NULL;
+  a->data = data;
+  a->inbound = inbound;
+  a->dup_to = dup_to;
+  *esp->argtail = a;
+  esp->argtail = &a->next;
+  return 0;
+}
+
+

+static void
+free_fd_data_map (struct fd_data_map_s *fd_data_map)
+{
+  int i;
+
+  if (!fd_data_map)
+    return;
+
+  for (i = 0; fd_data_map[i].data; i++)
+    {
+      if (fd_data_map[i].fd != -1)
+	_gpgme_io_close (fd_data_map[i].fd);
+      if (fd_data_map[i].peer_fd != -1)
+	_gpgme_io_close (fd_data_map[i].peer_fd);
+      /* Don't release data because this is only a reference.  */
+    }
+  free (fd_data_map);
+}
+
+
+static gpgme_error_t
+build_fd_data_map (engine_spawn_t esp)
+{
+  struct datalist_s *a;
+  size_t datac;
+  int fds[2];
+
+  for (datac = 0, a = esp->arglist; a; a = a->next)
+    if (a->data)
+      datac++;
+
+  free_fd_data_map (esp->fd_data_map);
+  esp->fd_data_map = calloc (datac + 1, sizeof *esp->fd_data_map);
+  if (!esp->fd_data_map)
+    return gpg_error_from_syserror ();
+
+  for (datac = 0, a = esp->arglist; a; a = a->next)
+    {
+      assert (a->data);
+
+      if (_gpgme_io_pipe (fds, a->inbound ? 1 : 0) == -1)
+        {
+          free (esp->fd_data_map);
+          esp->fd_data_map = NULL;
+          return gpg_error_from_syserror ();
+        }
+      if (_gpgme_io_set_close_notify (fds[0], close_notify_handler, esp)
+          || _gpgme_io_set_close_notify (fds[1], close_notify_handler, esp))
+        {
+          /* FIXME: Need error cleanup.  */
+          return gpg_error (GPG_ERR_GENERAL);
+        }
+
+      esp->fd_data_map[datac].inbound = a->inbound;
+      if (a->inbound)
+        {
+          esp->fd_data_map[datac].fd       = fds[0];
+          esp->fd_data_map[datac].peer_fd  = fds[1];
+        }
+      else
+        {
+          esp->fd_data_map[datac].fd       = fds[1];
+          esp->fd_data_map[datac].peer_fd  = fds[0];
+        }
+      esp->fd_data_map[datac].data    = a->data;
+      esp->fd_data_map[datac].dup_to  = a->dup_to;
+      datac++;
+    }
+
+  return 0;
+}
+
+
+static gpgme_error_t
+add_io_cb (engine_spawn_t esp, int fd, int dir, gpgme_io_cb_t handler,
+           void *data, void **tag)
+{
+  gpgme_error_t err;
+
+  err = (*esp->io_cbs.add) (esp->io_cbs.add_priv, fd, dir, handler, data, tag);
+  if (err)
+    return err;
+  if (!dir) /* Fixme: Kludge around poll() problem.  */
+    err = _gpgme_io_set_nonblocking (fd);
+  return err;
+}
+
+
+static gpgme_error_t
+engspawn_start (engine_spawn_t esp, const char *file, const char *argv[],
+                unsigned int flags)
+{
+  gpgme_error_t err;
+  int i, n;
+  int status;
+  struct spawn_fd_item_s *fd_list;
+  pid_t pid;
+  unsigned int spflags;
+
+  if (!esp || !file || !argv || !argv[0])
+    return gpg_error (GPG_ERR_INV_VALUE);
+
+  spflags = 0;
+  if ((flags & GPGME_SPAWN_DETACHED))
+    spflags |= IOSPAWN_FLAG_DETACHED;
+  if ((flags & GPGME_SPAWN_ALLOW_SET_FG))
+    spflags |= IOSPAWN_FLAG_ALLOW_SET_FG;
+
+
+  err = build_fd_data_map (esp);
+  if (err)
+    return err;
+
+  n = 0;
+  for (i = 0; esp->fd_data_map[i].data; i++)
+    n++;
+  fd_list = calloc (n, sizeof *fd_list);
+  if (!fd_list)
+    return gpg_error_from_syserror ();
+
+  /* Build the fd list for the child.  */
+  n = 0;
+  for (i = 0; esp->fd_data_map[i].data; i++)
+    {
+      fd_list[n].fd = esp->fd_data_map[i].peer_fd;
+      fd_list[n].dup_to = esp->fd_data_map[i].dup_to;
+      n++;
+    }
+  fd_list[n].fd = -1;
+  fd_list[n].dup_to = -1;
+
+  status = _gpgme_io_spawn (file, (char * const *)argv, spflags,
+                            fd_list, NULL, NULL, &pid);
+  free (fd_list);
+  if (status == -1)
+    return gpg_error_from_syserror ();
+
+  for (i = 0; esp->fd_data_map[i].data; i++)
+    {
+      err = add_io_cb (esp, esp->fd_data_map[i].fd,
+                       esp->fd_data_map[i].inbound,
+                       esp->fd_data_map[i].inbound
+                       ? _gpgme_data_inbound_handler
+                       : _gpgme_data_outbound_handler,
+                       esp->fd_data_map[i].data, &esp->fd_data_map[i].tag);
+      if (err)
+        return err;  /* FIXME: kill the child */
+    }
+
+  engspawn_io_event (esp, GPGME_EVENT_START, NULL);
+
+  return 0;
+}
+
+
+

+/*
+    Public functions
+ */
+
+static const char *
+engspawn_get_file_name (void)
+{
+  return "/nonexistent";
+}
+
+
+static char *
+engspawn_get_version (const char *file_name)
+{
+  (void)file_name;
+  return strdup ("1.0");
+}
+
+
+static const char *
+engspawn_get_req_version (void)
+{
+  return "1.0";
+}
+
+
+static gpgme_error_t
+engspawn_new (void **engine, const char *file_name, const char *home_dir)
+{
+  engine_spawn_t esp;
+
+  (void)file_name;
+  (void)home_dir;
+
+  esp = calloc (1, sizeof *esp);
+  if (!esp)
+    return gpg_error_from_syserror ();
+
+  esp->argtail = &esp->arglist;
+  *engine = esp;
+  return 0;
+}
+
+
+static void
+engspawn_release (void *engine)
+{
+  engine_spawn_t esp = engine;
+
+  if (!esp)
+    return;
+
+  engspawn_cancel (engine);
+
+  while (esp->arglist)
+    {
+      struct datalist_s *next = esp->arglist->next;
+
+      if (esp->arglist)
+	free (esp->arglist);
+      esp->arglist = next;
+    }
+
+  free (esp);
+}
+
+
+static void
+engspawn_set_io_cbs (void *engine, gpgme_io_cbs_t io_cbs)
+{
+  engine_spawn_t esp = engine;
+
+  esp->io_cbs = *io_cbs;
+}
+
+
+static void
+engspawn_io_event (void *engine, gpgme_event_io_t type, void *type_data)
+{
+  engine_spawn_t esp = engine;
+
+  TRACE3 (DEBUG_ENGINE, "gpgme:engspawn_io_event", esp,
+          "event %p, type %d, type_data %p",
+          esp->io_cbs.event, type, type_data);
+  if (esp->io_cbs.event)
+    (*esp->io_cbs.event) (esp->io_cbs.event_priv, type, type_data);
+}
+
+
+static gpgme_error_t
+engspawn_cancel (void *engine)
+{
+  engine_spawn_t esp = engine;
+
+  if (!esp)
+    return gpg_error (GPG_ERR_INV_VALUE);
+
+  if (esp->fd_data_map)
+    {
+      free_fd_data_map (esp->fd_data_map);
+      esp->fd_data_map = NULL;
+    }
+
+  return 0;
+}
+
+
+static gpgme_error_t
+engspawn_op_spawn (void *engine,
+                   const char *file, const char *argv[],
+                   gpgme_data_t datain,
+                   gpgme_data_t dataout, gpgme_data_t dataerr,
+                   unsigned int flags)
+{
+  engine_spawn_t esp = engine;
+  gpgme_error_t err = 0;
+
+  if (datain)
+    err = add_data (esp, datain, 0, 0);
+  if (!err && dataout)
+    err = add_data (esp, dataout, 1, 1);
+  if (!err && dataerr)
+    err = add_data (esp, dataerr, 2, 1);
+
+  if (!err)
+    err = engspawn_start (esp, file, argv, flags);
+
+  return err;
+}
+
+
+

+struct engine_ops _gpgme_engine_ops_spawn =
+  {
+    /* Static functions.  */
+    engspawn_get_file_name,
+    NULL,               /* get_home_dir */
+    engspawn_get_version,
+    engspawn_get_req_version,
+    engspawn_new,
+
+    /* Member functions.  */
+    engspawn_release,
+    NULL,		/* reset */
+    NULL,		/* set_status_handler */
+    NULL,		/* set_command_handler */
+    NULL,		/* set_colon_line_handler */
+    NULL,		/* set_locale */
+    NULL,		/* set_protocol */
+    NULL,		/* decrypt */
+    NULL,		/* decrypt_verify */
+    NULL,		/* delete */
+    NULL,		/* edit */
+    NULL,		/* encrypt */
+    NULL,		/* encrypt_sign */
+    NULL,		/* export */
+    NULL,		/* export_ext */
+    NULL,		/* genkey */
+    NULL,		/* import */
+    NULL,		/* keylist */
+    NULL,		/* keylist_ext */
+    NULL,		/* sign */
+    NULL,		/* trustlist */
+    NULL,		/* verify */
+    NULL,		/* getauditlog */
+    NULL,               /* opassuan_transact */
+    NULL,		/* conf_load */
+    NULL,		/* conf_save */
+    engspawn_set_io_cbs,
+    engspawn_io_event,	/* io_event */
+    engspawn_cancel,	/* cancel */
+    NULL,               /* cancel_op */
+    NULL,               /* passwd */
+    NULL,               /* set_pinentry_mode */
+    engspawn_op_spawn   /* opspawn */
+  };
diff --git a/src/engine-uiserver.c b/src/engine-uiserver.c
index bd140f9..2738c36 100644
--- a/src/engine-uiserver.c
+++ b/src/engine-uiserver.c
@@ -1340,5 +1340,6 @@ struct engine_ops _gpgme_engine_ops_uiserver =
     uiserver_cancel,
     NULL,		/* cancel_op */
     NULL,               /* passwd */
-    NULL                /* set_pinentry_mode */
+    NULL,                /* set_pinentry_mode */
+    NULL                /* opspawn */
   };
diff --git a/src/engine.c b/src/engine.c
index 4f2000c..f503430 100644
--- a/src/engine.c
+++ b/src/engine.c
@@ -51,10 +51,11 @@ static struct engine_ops *engine_ops[] =
     &_gpgme_engine_ops_assuan,		/* Low-Level Assuan.  */
     &_gpgme_engine_ops_g13,		/* Crypto VFS.  */
 #ifdef ENABLE_UISERVER
-    &_gpgme_engine_ops_uiserver		/* UI-Server.  */
+    &_gpgme_engine_ops_uiserver,	/* UI-Server.  */
 #else
-    NULL
+    NULL,
 #endif
+    &_gpgme_engine_ops_spawn
   };
 
 
@@ -193,7 +194,8 @@ gpgme_get_engine_info (gpgme_engine_info_t *info)
 					GPGME_PROTOCOL_GPGCONF,
 					GPGME_PROTOCOL_ASSUAN,
 					GPGME_PROTOCOL_G13,
-					GPGME_PROTOCOL_UISERVER };
+					GPGME_PROTOCOL_UISERVER,
+                                        GPGME_PROTOCOL_SPAWN    };
       unsigned int proto;
 
       err = 0;
@@ -936,3 +938,20 @@ _gpgme_engine_set_pinentry_mode (engine_t engine, gpgme_pinentry_mode_t mode)
 
   return (*engine->ops->set_pinentry_mode) (engine->engine, mode);
 }
+
+
+gpgme_error_t
+_gpgme_engine_op_spawn (engine_t engine,
+                        const char *file, const char *argv[],
+                        gpgme_data_t datain,
+                        gpgme_data_t dataout, gpgme_data_t dataerr)
+{
+  if (!engine)
+    return gpg_error (GPG_ERR_INV_VALUE);
+
+  if (!engine->ops->opspawn)
+    return gpg_error (GPG_ERR_NOT_IMPLEMENTED);
+
+  return (*engine->ops->opspawn) (engine->engine, file, argv,
+                                  datain, dataout, dataerr);
+}
diff --git a/src/engine.h b/src/engine.h
index a0287ad..ade7de1 100644
--- a/src/engine.h
+++ b/src/engine.h
@@ -163,5 +163,11 @@ gpgme_error_t _gpgme_engine_op_passwd (engine_t engine, gpgme_key_t key,
 gpgme_error_t _gpgme_engine_set_pinentry_mode (engine_t engine,
                                                gpgme_pinentry_mode_t mode);
 
+gpgme_error_t _gpgme_engine_op_spawn (engine_t engine,
+                                      const char *file, const char *argv[],
+                                      gpgme_data_t datain,
+                                      gpgme_data_t dataout,
+                                      gpgme_data_t dataerr);
+
 
 #endif /* ENGINE_H */
diff --git a/src/gpgme-tool.c b/src/gpgme-tool.c
index 2bf7654..a980347 100644
--- a/src/gpgme-tool.c
+++ b/src/gpgme-tool.c
@@ -1742,6 +1742,8 @@ gt_protocol_from_name (const char *name)
     return GPGME_PROTOCOL_G13;
   if (! strcasecmp (name, gpgme_get_protocol_name (GPGME_PROTOCOL_UISERVER)))
     return GPGME_PROTOCOL_UISERVER;
+  if (! strcasecmp (name, gpgme_get_protocol_name (GPGME_PROTOCOL_SPAWN)))
+    return GPGME_PROTOCOL_SPAWN;
   if (! strcasecmp (name, gpgme_get_protocol_name (GPGME_PROTOCOL_DEFAULT)))
     return GPGME_PROTOCOL_DEFAULT;
   return GPGME_PROTOCOL_UNKNOWN;
@@ -2106,6 +2108,18 @@ gt_identify (gpgme_tool_t gt, gpgme_data_t data)
 }
 
 
+gpg_error_t
+gt_spawn (gpgme_tool_t gt, const char *pgm,
+          gpgme_data_t inp, gpgme_data_t outp)
+{
+  gpg_error_t err;
+
+  err = gpgme_op_spawn (gt->ctx, pgm, NULL, inp, outp, outp, 0);
+
+  return err;
+}
+
+
 #define GT_RESULT_ENCRYPT 0x1
 #define GT_RESULT_DECRYPT 0x2
 #define GT_RESULT_SIGN 0x4
@@ -3487,6 +3501,55 @@ cmd_identify (assuan_context_t ctx, char *line)
 }
 
 
+static const char hlp_spawn[] =
+  "SPAWN PGM [args]\n"
+  "\n"
+  "Run program PGM with stdin connected to the INPUT source;\n"
+  "stdout and stderr to the OUTPUT source.";
+static gpg_error_t
+cmd_spawn (assuan_context_t ctx, char *line)
+{
+  struct server *server = assuan_get_pointer (ctx);
+  gpg_error_t err;
+  assuan_fd_t inp_fd;
+  char *inp_fn;
+  assuan_fd_t out_fd;
+  char *out_fn;
+  gpgme_data_t inp_data = NULL;
+  gpgme_data_t out_data = NULL;
+
+  inp_fd = server->input_fd;
+  inp_fn = server->input_filename;
+  out_fd = server->output_fd;
+  out_fn = server->output_filename;
+  if (inp_fd != ASSUAN_INVALID_FD || inp_fn)
+    {
+      err = server_data_obj (inp_fd, inp_fn, 0, server->input_enc, &inp_data,
+			     &server->input_stream);
+      if (err)
+	return err;
+    }
+  if (out_fd != ASSUAN_INVALID_FD || out_fn)
+    {
+      err = server_data_obj (out_fd, out_fn, 1, server->output_enc, &out_data,
+			     &server->output_stream);
+      if (err)
+	{
+	  gpgme_data_release (inp_data);
+	  return err;
+	}
+    }
+
+  err = gt_spawn (server->gt, line, inp_data, out_data);
+
+  gpgme_data_release (inp_data);
+  gpgme_data_release (out_data);
+
+  server_reset_fds (server);
+
+  return err;
+}
+
 
 /* Tell the assuan library about our commands.  */
 static gpg_error_t
@@ -3547,6 +3610,7 @@ register_commands (assuan_context_t ctx)
     { "HASH_ALGO_NAME", cmd_hash_algo_name },
     { "PASSWD", cmd_passwd, hlp_passwd },
     { "IDENTIFY", cmd_identify, hlp_identify },
+    { "SPAWN", cmd_spawn, hlp_spawn },
     { NULL }
   };
   int idx;
diff --git a/src/gpgme.c b/src/gpgme.c
index 438fef2..24b04fc 100644
--- a/src/gpgme.c
+++ b/src/gpgme.c
@@ -1,6 +1,7 @@
 /* gpgme.c - GnuPG Made Easy.
    Copyright (C) 2000 Werner Koch (dd9jn)
-   Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007, 2012 g10 Code GmbH
+   Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007, 2012,
+                 2014 g10 Code GmbH
 
    This file is part of GPGME.
 
@@ -15,9 +16,8 @@
    Lesser General Public License for more details.
 
    You should have received a copy of the GNU Lesser General Public
-   License along with this program; if not, write to the Free Software
-   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
-   02111-1307, USA.  */
+   License along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
 
 #if HAVE_CONFIG_H
 #include <config.h>
@@ -321,7 +321,8 @@ gpgme_set_protocol (gpgme_ctx_t ctx, gpgme_protocol_t protocol)
       && protocol != GPGME_PROTOCOL_GPGCONF
       && protocol != GPGME_PROTOCOL_ASSUAN
       && protocol != GPGME_PROTOCOL_G13
-      && protocol != GPGME_PROTOCOL_UISERVER)
+      && protocol != GPGME_PROTOCOL_UISERVER
+      && protocol != GPGME_PROTOCOL_SPAWN)
     return TRACE_ERR (gpg_error (GPG_ERR_INV_VALUE));
 
   if (!ctx)
@@ -405,6 +406,9 @@ gpgme_get_protocol_name (gpgme_protocol_t protocol)
     case GPGME_PROTOCOL_UISERVER:
       return "UIServer";
 
+    case GPGME_PROTOCOL_SPAWN:
+      return "Spawn";
+
     case GPGME_PROTOCOL_DEFAULT:
       return "default";
 
diff --git a/src/gpgme.def b/src/gpgme.def
index ee0c42e..dc18948 100644
--- a/src/gpgme.def
+++ b/src/gpgme.def
@@ -215,5 +215,7 @@ EXPORTS
 
     gpgme_get_dirinfo                     @162
 
+    gpgme_op_spawn_start                  @163
+    gpgme_op_spawn                        @164
 ; END
 
diff --git a/src/gpgme.h.in b/src/gpgme.h.in
index 050051a..655602d 100644
--- a/src/gpgme.h.in
+++ b/src/gpgme.h.in
@@ -1,7 +1,7 @@
 /* gpgme.h - Public interface to GnuPG Made Easy.                   -*- c -*-
    Copyright (C) 2000 Werner Koch (dd9jn)
    Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007, 2009
-                 2010, 2011, 2012, 2013 g10 Code GmbH
+                 2010, 2011, 2012, 2013, 2014 g10 Code GmbH
 
    This file is part of GPGME.
 
@@ -354,6 +354,7 @@ typedef enum
     GPGME_PROTOCOL_ASSUAN  = 3,  /* Low-level access to an Assuan server.  */
     GPGME_PROTOCOL_G13     = 4,
     GPGME_PROTOCOL_UISERVER= 5,
+    GPGME_PROTOCOL_SPAWN   = 6,  /* Direct access to any program.  */
     GPGME_PROTOCOL_DEFAULT = 254,
     GPGME_PROTOCOL_UNKNOWN = 255
   }
@@ -1694,6 +1695,26 @@ gpgme_error_t gpgme_op_card_edit (gpgme_ctx_t ctx, gpgme_key_t key,
 				  gpgme_edit_cb_t fnc, void *fnc_value,
 				  gpgme_data_t out);
 
+
+/* Flags for the spawn operations.  */
+#define GPGME_SPAWN_DETACHED      1
+#define GPGME_SPAWN_ALLOW_SET_FG  2
+
+
+/* Run the command FILE with the arguments in ARGV.  Connect stdin to
+   DATAIN, stdout to DATAOUT, and STDERR to DATAERR.  If one the data
+   streams is NULL, connect to /dev/null instead.  */
+gpgme_error_t gpgme_op_spawn_start (gpgme_ctx_t ctx,
+                                    const char *file, const char *argv[],
+                                    gpgme_data_t datain,
+                                    gpgme_data_t dataout, gpgme_data_t dataerr,
+                                    unsigned int flags);
+gpgme_error_t gpgme_op_spawn (gpgme_ctx_t ctx,
+                              const char *file, const char *argv[],
+                              gpgme_data_t datain,
+                              gpgme_data_t dataout, gpgme_data_t dataerr,
+                              unsigned int flags);
+
 

 /* Key management functions.  */
 struct _gpgme_op_keylist_result
diff --git a/src/libgpgme.vers b/src/libgpgme.vers
index 4db1d40..39663c1 100644
--- a/src/libgpgme.vers
+++ b/src/libgpgme.vers
@@ -89,6 +89,9 @@ GPGME_1.1 {
     gpgme_get_pinentry_mode;
 
     gpgme_get_dirinfo;
+
+    gpgme_op_spawn_start;
+    gpgme_op_spawn;
 };
 
 
diff --git a/src/spawn.c b/src/spawn.c
new file mode 100644
index 0000000..e3454f3
--- /dev/null
+++ b/src/spawn.c
@@ -0,0 +1,105 @@
+/* spawn.c - Run an arbitrary command with callbacks.
+   Copyright (C) 2014 Code GmbH
+
+   This file is part of GPGME.
+
+   GPGME 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.1 of
+   the License, or (at your option) any later version.
+
+   GPGME 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 program; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+   02111-1307, USA.  */
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <stdlib.h>
+
+#include "gpgme.h"
+#include "debug.h"
+#include "context.h"
+#include "util.h"
+#include "ops.h"
+
+
+static gpgme_error_t
+spawn_start (gpgme_ctx_t ctx, int synchronous,
+             const char *file, const char *argv[],
+             gpgme_data_t datain,
+             gpgme_data_t dataout, gpgme_data_t dataerr)
+{
+  gpgme_error_t err;
+  const char *tmp_argv[2];
+
+  if (ctx->protocol != GPGME_PROTOCOL_SPAWN)
+    return gpg_error (GPG_ERR_UNSUPPORTED_PROTOCOL);
+
+  err = _gpgme_op_reset (ctx, synchronous);
+  if (err)
+    return err;
+
+  if (!argv)
+    {
+      tmp_argv[0] = _gpgme_get_basename (file);
+      tmp_argv[1] = NULL;
+      argv = tmp_argv;
+    }
+
+  return _gpgme_engine_op_spawn (ctx->engine, file, argv,
+                                 datain, dataout, dataerr);
+}
+
+
+/* Run the command FILE with the arguments in ARGV.  Connect stdin to
+   DATAIN, stdout to DATAOUT, and STDERR to DATAERR.  If one the data
+   streams is NULL, connect to /dev/null instead.  */
+gpgme_error_t
+gpgme_op_spawn_start (gpgme_ctx_t ctx, const char *file, const char *argv[],
+                      gpgme_data_t datain,
+                      gpgme_data_t dataout, gpgme_data_t dataerr,
+                      unsigned int flags)
+{
+  gpgme_error_t err;
+
+  TRACE_BEG2 (DEBUG_CTX, "gpgme_op_spawn_start", ctx, "file=(%s) flaggs=%x",
+              file, flags);
+
+  if (!ctx)
+    return TRACE_ERR (gpg_error (GPG_ERR_INV_VALUE));
+
+  err = spawn_start (ctx, 0, file, argv, datain, dataout, dataerr);
+  return err;
+}
+
+
+/* Run the command FILE with the arguments in ARGV.  Connect stdin to
+   DATAIN, stdout to DATAOUT, and STDERR to DATAERR.  If one the data
+   streams is NULL, connect to /dev/null instead.  Synchronous
+   variant. */
+gpgme_error_t
+gpgme_op_spawn (gpgme_ctx_t ctx, const char *file, const char *argv[],
+	        gpgme_data_t datain,
+                gpgme_data_t dataout, gpgme_data_t dataerr,
+                unsigned int flags)
+{
+  gpgme_error_t err;
+
+  TRACE_BEG2 (DEBUG_CTX, "gpgme_op_spawn", ctx, "file=(%s) flags=%x",
+              file, flags);
+  if (!ctx)
+    return TRACE_ERR (gpg_error (GPG_ERR_INV_VALUE));
+
+  err = spawn_start (ctx, 1, file, argv, datain, dataout, dataerr);
+
+  if (!err)
+    err = _gpgme_wait_one (ctx);
+  return TRACE_ERR (err);
+}

commit 77931a9a1468b0cfbaafd4153867d90764a6d258
Author: Werner Koch <wk at gnupg.org>
Date:   Thu Apr 10 11:47:19 2014 +0200

    Add gpgme_get_dirinfo.
    
    * src/dirinfo.c (gpgme_get_dirinfo): New.
    * tests/t-engine-info.c (main): Print results from that function.

diff --git a/NEWS b/NEWS
index 8f9127e..f625ab9 100644
--- a/NEWS
+++ b/NEWS
@@ -5,10 +5,13 @@ Noteworthy changes in version 1.5.0 (unreleased)
    located via the envvar PATH.  All configuration options to set the
    name of the engines for configure run are removed.
 
- * If GPGME find the gpgconf binary it defaults to using gpg2 or
+ * If GPGME finds the gpgconf binary it defaults to using gpg2 or
    whatever gpgconf tells as name for the OpenPGP engine.  If gpgconf
    is not found, GPGME looks for an engine named "gpg".
 
+ * Interface changes relative to the 1.4.3 release:
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ gpgme_get_dirinfo              NEW.
 
 
 Noteworthy changes in version 1.4.3 (2013-08-12)
diff --git a/doc/gpgme.texi b/doc/gpgme.texi
index 4425396..e12fd73 100644
--- a/doc/gpgme.texi
+++ b/doc/gpgme.texi
@@ -878,6 +878,41 @@ allocated string describing the protocol @var{protocol}, or
 @section Engine Version Check
 @cindex version check, of the engines
 
+ at deftypefun @w{const char *} gpgme_get_dirinfo (@w{cons char *@var{what}})
+The function @code{gpgme_get_dirinfo} returns a statically allocated
+string with the value associated to @var{what}.  The returned values
+are the defaults and won't change even after
+ at code{gpgme_set_engine_info} has been used to configure a different
+engine.  @code{NULL} is returned if no value is available.  Commonly
+supported values for @var{what} are:
+
+ at table @code
+ at item homedir
+Return the default home directory.
+
+ at item agent-socket
+Return the name of the socket to connect to the gpg-agent.
+
+ at item uiserver-socket
+Return the name of the socket to connect to the user interface server.
+
+ at item gpgconf-name
+Return the file name of the engine configuration tool.
+
+ at item gpg-name
+Return the file name of the OpenPGP engine.
+
+ at item gpgsm-name
+Return the file name of the CMS engine.
+
+ at item g13-name
+Return the name of the file container encryption engine.
+
+ at end table
+
+ at end deftypefun
+
+
 @deftypefun gpgme_error_t gpgme_engine_check_version (@w{gpgme_protocol_t @var{protocol}})
 The function @code{gpgme_engine_check_version} verifies that the
 engine implementing the protocol @var{PROTOCOL} is installed in the
@@ -916,7 +951,8 @@ reserved for future use, so always check before you use it.
 @item const char *home_dir
 This is a string holding the directory name of the crypto engine's
 configuration directory.  If it is @code{NULL}, then the default
-directory is used.
+directory is used.  See @code{gpgme_get_dirinfo} on how to get the
+default directory.
 
 @item const char *version
 This is a string containing the version number of the crypto engine.
diff --git a/src/dirinfo.c b/src/dirinfo.c
index 845bc48..8824c9a 100644
--- a/src/dirinfo.c
+++ b/src/dirinfo.c
@@ -357,3 +357,28 @@ _gpgme_get_basename (const char *name)
       return s+1;
   return name;
 }
+
+
+/* Return default values for various directories and file names.  */
+const char *
+gpgme_get_dirinfo (const char *what)
+{
+  if (!what)
+    return NULL;
+  else if (!strcmp (what, "homedir"))
+    return get_gpgconf_item (WANT_HOMEDIR);
+  else if (!strcmp (what, "agent-socket"))
+    return get_gpgconf_item (WANT_AGENT_SOCKET);
+  else if (!strcmp (what, "uiserver-socket"))
+    return get_gpgconf_item (WANT_UISRV_SOCKET);
+  else if (!strcmp (what, "gpgconf-name"))
+    return get_gpgconf_item (WANT_GPGCONF_NAME);
+  else if (!strcmp (what, "gpg-name"))
+    return get_gpgconf_item (WANT_GPG_NAME);
+  else if (!strcmp (what, "gpgsm-name"))
+    return get_gpgconf_item (WANT_GPGSM_NAME);
+  else if (!strcmp (what, "g13-name"))
+    return get_gpgconf_item (WANT_G13_NAME);
+  else
+    return NULL;
+}
diff --git a/src/gpgme.def b/src/gpgme.def
index 0478cb6..ee0c42e 100644
--- a/src/gpgme.def
+++ b/src/gpgme.def
@@ -213,5 +213,7 @@ EXPORTS
 
     gpgme_data_identify                   @161
 
+    gpgme_get_dirinfo                     @162
+
 ; END
 
diff --git a/src/gpgme.h.in b/src/gpgme.h.in
index 5c4de6b..050051a 100644
--- a/src/gpgme.h.in
+++ b/src/gpgme.h.in
@@ -2090,6 +2090,9 @@ const char *gpgme_check_version_internal (const char *req_version,
   gpgme_check_version_internal (req_version,				\
 				offsetof (struct _gpgme_signature, validity))
 
+/* Return the default values for various directories.  */
+const char *gpgme_get_dirinfo (const char *what);
+
 /* Get the information about the configured and installed engines.  A
    pointer to the first engine in the statically allocated linked list
    is returned in *INFO.  If an error occurs, it is returned.  The
diff --git a/src/libgpgme.vers b/src/libgpgme.vers
index fe18e6a..4db1d40 100644
--- a/src/libgpgme.vers
+++ b/src/libgpgme.vers
@@ -87,6 +87,8 @@ GPGME_1.1 {
 
     gpgme_set_pinentry_mode;
     gpgme_get_pinentry_mode;
+
+    gpgme_get_dirinfo;
 };
 
 
diff --git a/tests/t-engine-info.c b/tests/t-engine-info.c
index 11fe4a4..53f5b2f 100644
--- a/tests/t-engine-info.c
+++ b/tests/t-engine-info.c
@@ -109,12 +109,26 @@ main (int argc, char **argv )
     }
 
   gpgme_check_version (NULL);
+
+  {
+    const char *keys[] = {"homedir", "agent-socket", "uiserver-socket",
+                          "gpgconf-name", "gpg-name", "gpgsm-name",
+                          "g13-name", NULL };
+    const char *s;
+    int i;
+
+    for (i=0; keys[i]; i++)
+      if ((s = gpgme_get_dirinfo (keys[i])))
+        fprintf (stderr, "dirinfo: %s='%s'\n", keys[i], s);
+  }
+
   err = gpgme_get_engine_info (&info);
   fail_if_err (err);
 
   for (; info; info = info->next)
-    fprintf (stdout, "protocol=%d engine='%s' v='%s' (min='%s')\n",
-             info->protocol, info->file_name, info->version, info->req_version);
+    fprintf (stdout, "protocol=%d engine='%s' v='%s' (min='%s') home='%s'\n",
+             info->protocol, info->file_name, info->version, info->req_version,
+             info->home_dir? info->home_dir : "[default]");
 
   return 0;
 }

commit b788c36ec05f73da3ca71aab337432adb203ffd9
Author: Werner Koch <wk at gnupg.org>
Date:   Thu Apr 10 11:42:39 2014 +0200

    Remove compiler warning from test
    
    --

diff --git a/tests/gpg/t-keylist-sig.c b/tests/gpg/t-keylist-sig.c
index 9482c74..ec30624 100644
--- a/tests/gpg/t-keylist-sig.c
+++ b/tests/gpg/t-keylist-sig.c
@@ -3,17 +3,17 @@
    Copyright (C) 2001, 2003, 2004 g10 Code GmbH
 
    This file is part of GPGME.
- 
+
    GPGME 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.1 of
    the License, or (at your option) any later version.
-   
+
    GPGME 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 program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
@@ -53,7 +53,7 @@ struct
       unsigned int sig_class;
       int exportable;
     } sig;
-  } uid[3];  
+  } uid[3];
 }
 keys[] =
   {
@@ -71,7 +71,7 @@ keys[] =
   };
 
 
-int 
+int
 main (int argc, char **argv)
 {
   gpgme_error_t err;
@@ -93,7 +93,7 @@ main (int argc, char **argv)
 
   err = gpgme_op_keylist_start (ctx, "Alpha", 0);
   fail_if_err (err);
-    
+
   while (!(err = gpgme_op_keylist_next (ctx, &key)))
     {
       if (!keys[i].fpr)
@@ -462,12 +462,9 @@ main (int argc, char **argv)
 		   key->uids->next->email);
 	  exit (1);
 	}
-      /*FIXME: There is a bug in gpg 1.3.4 which duplicates a signaure
-        after importing the secret key.  We disable this test for
-        now. */
-#ifdef __GNUC__
-#warning test disabled due to problems with gpg 1.3.4 generated key
-#endif
+      /* Note: There is a bug in gpg 1.3.4 which duplicates a
+         signature after importing the secret key.  Thus we disable
+         the second part of the check. */
       if (key->uids && (!key->uids->next->signatures /*|| key->uids->next->signatures->next*/))
 	{
 	  fprintf (stderr, "Second user ID unexpected number of signatures\n");

commit 443f6b76a964b77e2e61c03592a83ffa0a6da4b1
Author: Werner Koch <wk at gnupg.org>
Date:   Thu Apr 10 11:39:14 2014 +0200

    Make use of internal iospawn flags more flexible.
    
    * src/priv-io.h (IOSPAWN_FLAG_DETACHED): New. Renumber the others.
    * src/w32-io.c (_gpgme_io_spawn): Use DETACHED_PROCESS process only if
    IOSPAWN_FLAG_DETACHED is given.
    * src/w32-qt-io.cpp (_gpgme_io_spawn): Ditto.
    * src/w32-glib-io.c (_gpgme_io_spawn): Ditto.
    * src/assuan-support.c (my_spawn): Pass IOSPAWN_FLAG_DETACHED flags.
    * src/dirinfo.c (read_gpgconf_dirs): Ditto.
    * src/engine-gpg.c (start): Ditto.
    * src/engine-gpgconf.c (gpgconf_read, gpgconf_write): Ditto.
    * src/version.c (_gpgme_get_program_version): Ditto.

diff --git a/src/assuan-support.c b/src/assuan-support.c
index d06518a..0a11d9f 100644
--- a/src/assuan-support.c
+++ b/src/assuan-support.c
@@ -168,7 +168,8 @@ my_spawn (assuan_context_t ctx, pid_t *r_pid, const char *name,
   fd_items[i].fd = -1;
   fd_items[i].dup_to = -1;
 
-  err = _gpgme_io_spawn (name, (char*const*)argv, IOSPAWN_FLAG_NOCLOSE,
+  err = _gpgme_io_spawn (name, (char*const*)argv,
+                         (IOSPAWN_FLAG_NOCLOSE | IOSPAWN_FLAG_DETACHED),
 			 fd_items, atfork, atforkvalue, r_pid);
   if (! err)
     {
diff --git a/src/dirinfo.c b/src/dirinfo.c
index 8526d39..845bc48 100644
--- a/src/dirinfo.c
+++ b/src/dirinfo.c
@@ -156,7 +156,8 @@ read_gpgconf_dirs (const char *pgmname, int components)
 
   cfd[0].fd = rp[1];
 
-  status = _gpgme_io_spawn (pgmname, argv, 0, cfd, NULL, NULL, NULL);
+  status = _gpgme_io_spawn (pgmname, argv, IOSPAWN_FLAG_DETACHED,
+                            cfd, NULL, NULL, NULL);
   if (status < 0)
     {
       _gpgme_io_close (rp[0]);
diff --git a/src/engine-gpg.c b/src/engine-gpg.c
index 6b04e18..9037dd7 100644
--- a/src/engine-gpg.c
+++ b/src/engine-gpg.c
@@ -1359,7 +1359,7 @@ start (engine_gpg_t gpg)
   fd_list[n].dup_to = -1;
 
   status = _gpgme_io_spawn (pgmname, gpg->argv,
-                            IOSPAWN_FLAG_ALLOW_SET_FG,
+                            (IOSPAWN_FLAG_DETACHED |IOSPAWN_FLAG_ALLOW_SET_FG),
                             fd_list, NULL, NULL, &pid);
   {
     int saved_err = gpg_error_from_syserror ();
diff --git a/src/engine-gpgconf.c b/src/engine-gpgconf.c
index 47cde66..811ad9d 100644
--- a/src/engine-gpgconf.c
+++ b/src/engine-gpgconf.c
@@ -228,7 +228,8 @@ gpgconf_read (void *engine, char *arg1, char *arg2,
 
   cfd[0].fd = rp[1];
 
-  status = _gpgme_io_spawn (gpgconf->file_name, argv, 0, cfd, NULL, NULL, NULL);
+  status = _gpgme_io_spawn (gpgconf->file_name, argv,
+                            IOSPAWN_FLAG_DETACHED, cfd, NULL, NULL, NULL);
   if (status < 0)
     {
       _gpgme_io_close (rp[0]);
@@ -697,7 +698,8 @@ gpgconf_write (void *engine, char *arg1, char *arg2, gpgme_data_t conf)
 
   cfd[0].fd = rp[0];
 
-  status = _gpgme_io_spawn (gpgconf->file_name, argv, 0, cfd, NULL, NULL, NULL);
+  status = _gpgme_io_spawn (gpgconf->file_name, argv,
+                            IOSPAWN_FLAG_DETACHED, cfd, NULL, NULL, NULL);
   if (status < 0)
     {
       _gpgme_io_close (rp[0]);
diff --git a/src/posix-io.c b/src/posix-io.c
index afee504..908c1ee 100644
--- a/src/posix-io.c
+++ b/src/posix-io.c
@@ -372,8 +372,6 @@ _gpgme_io_spawn (const char *path, char *const argv[], unsigned int flags,
   int status;
   int signo;
 
-  (void)flags;
-
   TRACE_BEG1 (DEBUG_SYSIO, "_gpgme_io_spawn", path,
 	      "path=%s", path);
   i = 0;
diff --git a/src/priv-io.h b/src/priv-io.h
index 4058b3b..583f06a 100644
--- a/src/priv-io.h
+++ b/src/priv-io.h
@@ -75,11 +75,13 @@ int _gpgme_io_set_close_notify (int fd, _gpgme_close_notify_handler_t handler,
 				void *value);
 int _gpgme_io_set_nonblocking (int fd);
 
+/* Under Windows do not allocate a console.  */
+#define IOSPAWN_FLAG_DETACHED 1
 /* A flag to tell the spawn function to allow the child process to set
    the foreground window. */
-#define IOSPAWN_FLAG_ALLOW_SET_FG 1
+#define IOSPAWN_FLAG_ALLOW_SET_FG 2
 /* Don't close any child FDs.  */
-#define IOSPAWN_FLAG_NOCLOSE 2
+#define IOSPAWN_FLAG_NOCLOSE 4
 
 /* Spawn the executable PATH with ARGV as arguments.  After forking
    close all fds except for those in FD_LIST in the child, then
diff --git a/src/version.c b/src/version.c
index 18825f8..15e5aee 100644
--- a/src/version.c
+++ b/src/version.c
@@ -321,7 +321,8 @@ _gpgme_get_program_version (const char *const file_name)
 
   cfd[0].fd = rp[1];
 
-  status = _gpgme_io_spawn (file_name, argv, 0, cfd, NULL, NULL, NULL);
+  status = _gpgme_io_spawn (file_name, argv,
+                            IOSPAWN_FLAG_DETACHED, cfd, NULL, NULL, NULL);
   if (status < 0)
     {
       _gpgme_io_close (rp[0]);
diff --git a/src/w32-glib-io.c b/src/w32-glib-io.c
index cb1bb3c..a5af4e6 100644
--- a/src/w32-glib-io.c
+++ b/src/w32-glib-io.c
@@ -662,7 +662,8 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
   si.hStdError = INVALID_HANDLE_VALUE;
 
   cr_flags |= CREATE_SUSPENDED;
-  cr_flags |= DETACHED_PROCESS;
+  if ((flags & IOSPAWN_FLAG_DETACHED))
+    cr_flags |= DETACHED_PROCESS;
   if (!CreateProcessA (_gpgme_get_w32spawn_path (),
 		       arg_string,
 		       &sec_attr,     /* process security attributes */
diff --git a/src/w32-io.c b/src/w32-io.c
index d896ec0..634ecb3 100644
--- a/src/w32-io.c
+++ b/src/w32-io.c
@@ -1598,7 +1598,8 @@ _gpgme_io_spawn (const char *path, char *const argv[], unsigned int flags,
   si.hStdError = INVALID_HANDLE_VALUE;
 
   cr_flags |= CREATE_SUSPENDED;
-  cr_flags |= DETACHED_PROCESS;
+  if ((flags & IOSPAWN_FLAG_DETACHED))
+    cr_flags |= DETACHED_PROCESS;
   cr_flags |= GetPriorityClass (GetCurrentProcess ());
   if (!CreateProcessA (_gpgme_get_w32spawn_path (),
 		       arg_string,
diff --git a/src/w32-qt-io.cpp b/src/w32-qt-io.cpp
index 358ff75..44655ec 100644
--- a/src/w32-qt-io.cpp
+++ b/src/w32-qt-io.cpp
@@ -3,17 +3,17 @@
    Copyright (C) 2001, 2002, 2004, 2005, 2007 g10 Code GmbH
 
    This file is part of GPGME.
- 
+
    GPGME 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.1 of
    the License, or (at your option) any later version.
-   
+
    GPGME 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 program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
@@ -107,7 +107,7 @@ find_channel (int fd, int create)
     DeviceEntry* entry = new DeviceEntry;
     entry->iodev = new KDPipeIODevice
       (fd, QIODevice::ReadWrite|QIODevice::Unbuffered);
-    iodevice_table[fd] = entry; 
+    iodevice_table[fd] = entry;
   }
   return iodevice_table[fd] ? iodevice_table[fd]->iodev : 0;
 }
@@ -156,7 +156,7 @@ _gpgme_io_read (int fd, void *buffer, size_t count)
       errno = EAGAIN;
       return TRACE_SYSRES( -1 );
   }
- 
+
   nread = chan->read ((char *) buffer, count);
   if (nread < 0)
     {
@@ -289,8 +289,8 @@ _gpgme_io_close (int fd)
       notify_table[fd].value = NULL;
     }
 
-  /* Then do the close.  */    
-  
+  /* Then do the close.  */
+
   DeviceEntry* const entry = iodevice_table[fd];
   if ( entry ) {
       if ( entry->unref() == 0 ) {
@@ -303,7 +303,7 @@ _gpgme_io_close (int fd)
       _close( fd );
   }
 
-  
+
 
   return 0;
 }
@@ -334,7 +334,7 @@ _gpgme_io_set_nonblocking (int fd)
 {
   DeviceEntry* const entry = iodevice_table[fd];
   assert( entry );
-  entry->blocking = false; 
+  entry->blocking = false;
   TRACE_BEG (DEBUG_SYSIO, "_gpgme_io_set_nonblocking", fd);
   return TRACE_SYSRES (0);
 }
@@ -347,7 +347,7 @@ build_commandline (char **argv)
   int n = 0;
   char *buf;
   char *p;
-  
+
   /* We have to quote some things because under Windows the program
      parses the commandline and does some unquoting.  We enclose the
      whole argument in double-quotes, and escape literal double-quotes
@@ -428,7 +428,7 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
       TRACE_LOG2 ("argv[%2i] = %s", i, argv[i]);
       i++;
     }
-  
+
   /* We do not inherit any handles by default, and just insert those
      handles we want the child to have afterwards.  But some handle
      values occur on the command line, and we need to move
@@ -450,7 +450,7 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
   memset (&sec_attr, 0, sizeof sec_attr);
   sec_attr.nLength = sizeof sec_attr;
   sec_attr.bInheritHandle = FALSE;
-  
+
   arg_string = build_commandline (args);
   free (args);
   if (!arg_string)
@@ -459,7 +459,7 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
       DeleteFile (tmp_name);
       return TRACE_SYSRES (-1);
     }
-  
+
   memset (&si, 0, sizeof si);
   si.cb = sizeof (si);
   si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
@@ -469,7 +469,8 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
   si.hStdError = INVALID_HANDLE_VALUE;
 
   cr_flags |= CREATE_SUSPENDED;
-  cr_flags |= DETACHED_PROCESS;
+  if ((flags & IOSPAWN_FLAG_DETACHED))
+    cr_flags |= DETACHED_PROCESS;
   if (!CreateProcessA (_gpgme_get_w32spawn_path (),
 		       arg_string,
 		       &sec_attr,     /* process security attributes */
@@ -523,7 +524,7 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
       /* Return the child name of this handle.  */
       fd_list[i].peer_name = (int) hd;
     }
-    
+
   /* Write the handle translation information to the temporary
      file.  */
   {
@@ -545,7 +546,7 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
       {
 	/* Strip the newline.  */
 	len = strlen (line) - 1;
-	
+
 	/* Format is: Local name, stdin/stdout/stderr, peer name, argv idx.  */
 	snprintf (&line[len], BUFFER_MAX - len, "0x%x %d 0x%x %d  \n",
 		  fd_list[i].fd, fd_list[i].dup_to,
@@ -567,18 +568,18 @@ _gpgme_io_spawn (const char *path, char * const argv[], unsigned int flags,
   close (tmp_fd);
   /* The temporary file is deleted by the gpgme-w32spawn process
      (hopefully).  */
-  
+
   TRACE_LOG4 ("CreateProcess ready: hProcess=%p, hThread=%p, "
 	      "dwProcessID=%d, dwThreadId=%d",
-	      pi.hProcess, pi.hThread, 
+	      pi.hProcess, pi.hThread,
 	      (int) pi.dwProcessId, (int) pi.dwThreadId);
 
   if (r_pid)
     *r_pid = (pid_t)pi.dwProcessId;
-  
+
   if (ResumeThread (pi.hThread) < 0)
     TRACE_LOG1 ("ResumeThread failed: ec=%d", (int) GetLastError ());
-  
+
   if (!CloseHandle (pi.hThread))
     TRACE_LOG1 ("CloseHandle of thread failed: ec=%d",
 		(int) GetLastError ());
@@ -635,7 +636,7 @@ _gpgme_io_select (struct io_select_fd_s *fds, size_t nfds, int nonblock)
           else
               fds[i].signaled = chan->waitForReadyRead( 1000 ) ? 1 : 0;
 	  TRACE_ADD1 (dbg_help, "w0x%x ", fds[i].fd);
-          if ( fds[i].signaled ) 
+          if ( fds[i].signaled )
               count++;
         }
       else if (fds[i].for_write)
@@ -644,11 +645,11 @@ _gpgme_io_select (struct io_select_fd_s *fds, size_t nfds, int nonblock)
           assert (chan);
           fds[i].signaled = nonblock ? ( chan->writeWouldBlock() ? 0 : 1 ) : 1;
           TRACE_ADD1 (dbg_help, "w0x%x ", fds[i].fd);
-          if ( fds[i].signaled ) 
+          if ( fds[i].signaled )
               count++;
         }
     }
-  TRACE_END (dbg_help, "]"); 
+  TRACE_END (dbg_help, "]");
 
   return TRACE_SYSRES (count);
 }

commit 617d3be629229cbebfdc2d26a4e854bc4fe38a68
Author: Werner Koch <wk at gnupg.org>
Date:   Thu Apr 10 11:15:40 2014 +0200

    Make sure a spawned process has all standard fds connected.
    
    * src/posix-io.c (_gpgme_io_spawn): dup /dev/null also to unsued
    stdout.
    --
    
    Better be sure that stdout of a spawned process is connected to stdout
    so that the process does not run into a write error if it writes to
    stdout.  AFAICS we always use a connected stdout; thus this is only
    for correctness.

diff --git a/src/posix-io.c b/src/posix-io.c
index ceb8831..afee504 100644
--- a/src/posix-io.c
+++ b/src/posix-io.c
@@ -402,6 +402,7 @@ _gpgme_io_spawn (const char *path, char *const argv[], unsigned int flags,
 
 	  /* Child.  */
 	  int seen_stdin = 0;
+	  int seen_stdout = 0;
 	  int seen_stderr = 0;
 
 	  if (atfork)
@@ -430,6 +431,8 @@ _gpgme_io_spawn (const char *path, char *const argv[], unsigned int flags,
 
 	      if (child_fd == 0)
 		seen_stdin = 1;
+	      else if (child_fd == 1)
+		seen_stdout = 1;
 	      else if (child_fd == 2)
 		seen_stderr = 1;
 
@@ -451,56 +454,38 @@ _gpgme_io_spawn (const char *path, char *const argv[], unsigned int flags,
 	      close (fd_list[i].fd);
 	    }
 
-	  if (! seen_stdin || ! seen_stderr)
+	  if (! seen_stdin || ! seen_stdout || !seen_stderr)
 	    {
 	      fd = open ("/dev/null", O_RDWR);
 	      if (fd == -1)
 		{
-#if 0
-		  /* FIXME: The debug file descriptor is not dup'ed
-		     anyway, so we can't see this.  */
-		  TRACE_LOG1 ("can't open `/dev/null': %s\n",
-			      strerror (errno));
-#endif
+		  /* The debug file descriptor is not dup'ed, so we
+		     can't do a trace output.  */
 		  _exit (8);
 		}
-	      /* Make sure that the process has a connected stdin.  */
+	      /* Make sure that the process has connected stdin.  */
 	      if (! seen_stdin && fd != 0)
 		{
 		  if (dup2 (fd, 0) == -1)
-		    {
-#if 0
-		  /* FIXME: The debug file descriptor is not dup'ed
-		     anyway, so we can't see this.  */
-		      TRACE_LOG1 ("dup2(/dev/null, 0) failed: %s\n",
-				  strerror (errno));
-#endif
-		      _exit (8);
-		    }
+                    _exit (8);
 		}
+	      if (! seen_stdout && fd != 1)
+                {
+                  if (dup2 (fd, 1) == -1)
+                    _exit (8);
+                }
 	      if (! seen_stderr && fd != 2)
-		if (dup2 (fd, 2) == -1)
-		  {
-#if 0
-		    /* FIXME: The debug file descriptor is not dup'ed
-		       anyway, so we can't see this.  */
-		    TRACE_LOG1 ("dup2(dev/null, 2) failed: %s\n",
-				strerror (errno));
-#endif
-		    _exit (8);
-		  }
-	      if (fd != 0 && fd != 2)
+                {
+                  if (dup2 (fd, 2) == -1)
+                    _exit (8);
+                }
+	      if (fd != 0 && fd != 1 && fd != 2)
 		close (fd);
 	    }
 
 	  execv (path, (char *const *) argv);
 	  /* Hmm: in that case we could write a special status code to the
 	     status-pipe.  */
-#if 0
-	  /* FIXME: The debug file descriptor is not dup'ed anyway, so
-	     we can't see this.  */
-	  TRACE_LOG1 ("exec of `%s' failed\n", path);
-#endif
 	  _exit (8);
 	  /* End child.  */
 	}

-----------------------------------------------------------------------

Summary of changes:
 NEWS                      |   13 +-
 doc/gpgme.texi            |  114 ++++++++++-
 src/Makefile.am           |    3 +-
 src/assuan-support.c      |    3 +-
 src/dirinfo.c             |   28 ++-
 src/engine-assuan.c       |    3 +-
 src/engine-backend.h      |    9 +
 src/engine-g13.c          |    3 +-
 src/engine-gpg.c          |    5 +-
 src/engine-gpgconf.c      |    9 +-
 src/engine-gpgsm.c        |    3 +-
 src/engine-spawn.c        |  467 +++++++++++++++++++++++++++++++++++++++++++++
 src/engine-uiserver.c     |    3 +-
 src/engine.c              |   25 ++-
 src/engine.h              |    6 +
 src/gpgme-tool.c          |   64 +++++++
 src/gpgme.c               |   14 +-
 src/gpgme.def             |    4 +
 src/gpgme.h.in            |   26 ++-
 src/libgpgme.vers         |    5 +
 src/posix-io.c            |   53 ++---
 src/priv-io.h             |    6 +-
 src/spawn.c               |  105 ++++++++++
 src/version.c             |    3 +-
 src/w32-glib-io.c         |    3 +-
 src/w32-io.c              |    3 +-
 src/w32-qt-io.cpp         |   47 ++---
 tests/gpg/t-keylist-sig.c |   21 +-
 tests/t-engine-info.c     |   18 +-
 29 files changed, 963 insertions(+), 103 deletions(-)
 create mode 100644 src/engine-spawn.c
 create mode 100644 src/spawn.c


hooks/post-receive
-- 
GnuPG Made Easy
http://git.gnupg.org




More information about the Gnupg-commits mailing list