[gnome-keyring/wip/dueno/ssh-agent: 2/4] ssh-agent: rewrite subprocess handling



commit 66103745f0edf677c5ed506ff12236c64d70ce0a
Author: Daiki Ueno <dueno src gnome org>
Date:   Tue Dec 12 16:39:13 2017 +0100

    ssh-agent: rewrite subprocess handling

 daemon/ssh-agent/Makefile.am             |    4 +-
 daemon/ssh-agent/gkd-ssh-agent-client.c  |  214 --------------------
 daemon/ssh-agent/gkd-ssh-agent-client.h  |   33 ---
 daemon/ssh-agent/gkd-ssh-agent-ops.c     |   78 ++++++---
 daemon/ssh-agent/gkd-ssh-agent-private.h |    6 +-
 daemon/ssh-agent/gkd-ssh-agent-process.c |  311 ++++++++++++++++++++++++++++++
 daemon/ssh-agent/gkd-ssh-agent-process.h |   47 +++++
 daemon/ssh-agent/gkd-ssh-agent.c         |   17 +-
 8 files changed, 426 insertions(+), 284 deletions(-)
---
diff --git a/daemon/ssh-agent/Makefile.am b/daemon/ssh-agent/Makefile.am
index 9f5f1db..6b1e719 100644
--- a/daemon/ssh-agent/Makefile.am
+++ b/daemon/ssh-agent/Makefile.am
@@ -8,8 +8,8 @@ noinst_LTLIBRARIES += \
 libgkd_ssh_agent_la_SOURCES = \
        daemon/ssh-agent/gkd-ssh-agent.c \
        daemon/ssh-agent/gkd-ssh-agent.h \
-       daemon/ssh-agent/gkd-ssh-agent-client.h \
-       daemon/ssh-agent/gkd-ssh-agent-client.c \
+       daemon/ssh-agent/gkd-ssh-agent-process.h \
+       daemon/ssh-agent/gkd-ssh-agent-process.c \
        daemon/ssh-agent/gkd-ssh-agent-preload.h \
        daemon/ssh-agent/gkd-ssh-agent-preload.c \
        daemon/ssh-agent/gkd-ssh-agent-private.h \
diff --git a/daemon/ssh-agent/gkd-ssh-agent-ops.c b/daemon/ssh-agent/gkd-ssh-agent-ops.c
index 0669d0e..cde2f8c 100644
--- a/daemon/ssh-agent/gkd-ssh-agent-ops.c
+++ b/daemon/ssh-agent/gkd-ssh-agent-ops.c
@@ -23,7 +23,6 @@
 
 #include "config.h"
 
-#include "gkd-ssh-agent-client.h"
 #include "gkd-ssh-agent-preload.h"
 #include "gkd-ssh-agent-private.h"
 #include "gkd-ssh-interaction.h"
@@ -48,10 +47,16 @@ static gboolean
 op_add_identity (GkdSshAgentCall *call)
 {
        GList *keys;
-       gsize offset;
-       gconstpointer blob;
+       const guchar *blob;
+       gsize offset = 5;
        gsize length;
        GList *l;
+       GBytes *key = NULL;
+       gboolean ret;
+
+       /* If parsing the request fails, just pass through */
+       if (egg_buffer_get_byte_array (call->req, offset, &offset, &blob, &length))
+               key = g_bytes_new (blob, length);
 
        /*
         * Here we want to remove the preload key from our list, so that we
@@ -62,22 +67,28 @@ op_add_identity (GkdSshAgentCall *call)
         * because for RSA and DSA the private key pair coming in has the same
         * initial bytes as the public key we've loaded.
         */
-
-       keys = gkd_ssh_agent_preload_keys ();
-
-       offset = 5;
-       for (l = keys; l != NULL; l = g_list_next (l)) {
-               blob = g_bytes_get_data (l->data, &length);
-               if (call->req->len >= length + offset &&
-                   memcmp (call->req->buf + offset, blob, length) == 0) {
-                       gkd_ssh_agent_preload_clear (l->data);
-                       break;
+       if (key) {
+               keys = gkd_ssh_agent_preload_keys ();
+
+               for (l = keys; l != NULL; l = g_list_next (l)) {
+                       if (g_bytes_equal (l->data, key)) {
+                               gkd_ssh_agent_preload_clear (l->data);
+                               break;
+                       }
                }
+
+               g_list_free_full (keys, (GDestroyNotify)g_bytes_unref);
        }
 
-       g_list_free_full (keys, (GDestroyNotify)g_bytes_unref);
+       ret = gkd_ssh_agent_process_call (call->process, call->req, call->resp);
 
-       return gkd_ssh_agent_relay (call);
+       if (key) {
+               if (ret)
+                       gkd_ssh_agent_process_add_key (call->process, key);
+               g_bytes_unref (key);
+       }
+
+       return ret;
 }
 
 static GHashTable *
@@ -119,14 +130,16 @@ static gboolean
 op_request_identities (GkdSshAgentCall *call)
 {
        GHashTable *answer;
+       GHashTableIter iter;
        const guchar *blob;
        gchar *comment;
        gsize length;
        guint32 added;
+       GBytes *key;
        GList *keys;
        GList *l;
 
-       if (!gkd_ssh_agent_relay (call))
+       if (!gkd_ssh_agent_process_call (call->process, call->req, call->resp))
                return FALSE;
 
        /* Parse all the keys, and if it fails, just fall through */
@@ -134,6 +147,10 @@ op_request_identities (GkdSshAgentCall *call)
        if (!answer)
                return TRUE;
 
+       g_hash_table_iter_init (&iter, answer);
+       while (g_hash_table_iter_next (&iter, (gpointer *)&key, NULL))
+               gkd_ssh_agent_process_add_key (call->process, key);
+
        added = 0;
 
        /* Add any preloaded keys not already in answer */
@@ -162,7 +179,7 @@ op_request_identities (GkdSshAgentCall *call)
 }
 
 static void
-preload_key_if_necessary (gint ssh_agent,
+preload_key_if_necessary (GkdSshAgentCall *call,
                           GBytes *key)
 {
        GTlsInteraction *interaction;
@@ -181,6 +198,9 @@ preload_key_if_necessary (gint ssh_agent,
        if (!filename)
                return;
 
+       if (gkd_ssh_agent_process_lookup_key (call->process, key))
+               return;
+
        interaction = gkd_ssh_interaction_new (key);
        askpass = gcr_ssh_askpass_new (interaction);
        g_object_unref (interaction);
@@ -213,13 +233,13 @@ op_sign_request (GkdSshAgentCall *call)
        /* If parsing the request fails, just pass through */
        if (egg_buffer_get_byte_array (call->req, offset, &offset, &blob, &length)) {
                key = g_bytes_new (blob, length);
-               preload_key_if_necessary (call->ssh_agent, key);
+               preload_key_if_necessary (call, key);
                g_bytes_unref (key);
        } else {
                g_warning ("got unparseable sign request for ssh-agent");
        }
 
-       return gkd_ssh_agent_relay (call);
+       return gkd_ssh_agent_process_call (call->process, call->req, call->resp);
 }
 
 static gboolean
@@ -228,26 +248,38 @@ op_remove_identity (GkdSshAgentCall *call)
        const guchar *blob;
        gsize length;
        gsize offset = 5;
-       GBytes *key;
+       GBytes *key = NULL;
+       gboolean ret;
 
        /* If parsing the request fails, just pass through */
        if (egg_buffer_get_byte_array (call->resp, offset, &offset, &blob, &length)) {
                key = g_bytes_new (blob, length);
                gkd_ssh_agent_preload_clear (key);
-               g_bytes_unref (key);
        } else {
                g_warning ("got unparseable remove request for ssh-agent");
        }
 
        /* If the key doesn't exist what happens here? */
-       return gkd_ssh_agent_relay (call);
+       ret = gkd_ssh_agent_process_call (call->process, call->req, call->resp);
+       if (key) {
+               if (ret)
+                       gkd_ssh_agent_process_remove_key (call->process, key);
+               g_bytes_unref (key);
+       }
+       return ret;
 }
 
 static gboolean
 op_remove_all_identities (GkdSshAgentCall *call)
 {
        gkd_ssh_agent_preload_clear_all ();
-       return gkd_ssh_agent_relay (call);
+
+       if (gkd_ssh_agent_process_call (call->process, call->req, call->resp)) {
+               gkd_ssh_agent_process_clear_keys (call->process);
+               return TRUE;
+       }
+
+       return FALSE;
 }
 
 const GkdSshAgentOperation gkd_ssh_agent_operations[GKD_SSH_OP_MAX] = {
diff --git a/daemon/ssh-agent/gkd-ssh-agent-private.h b/daemon/ssh-agent/gkd-ssh-agent-private.h
index 159475e..0ca97ac 100644
--- a/daemon/ssh-agent/gkd-ssh-agent-private.h
+++ b/daemon/ssh-agent/gkd-ssh-agent-private.h
@@ -23,7 +23,7 @@
 #ifndef GKDSSHPRIVATE_H_
 #define GKDSSHPRIVATE_H_
 
-#include "gkd-ssh-agent-client.h"
+#include "gkd-ssh-agent-process.h"
 
 #include "egg/egg-buffer.h"
 
@@ -33,7 +33,7 @@ typedef struct _GkdSshAgentCall {
        int sock;
        EggBuffer *req;
        EggBuffer *resp;
-       gint ssh_agent;
+       GkdSshAgentProcess *process;
 } GkdSshAgentCall;
 
 /* -----------------------------------------------------------------------------
@@ -97,8 +97,6 @@ gboolean              gkd_ssh_agent_read_packet                     (gint fd,
 gboolean              gkd_ssh_agent_write_packet                    (gint fd,
                                                                      EggBuffer *buffer);
 
-gboolean              gkd_ssh_agent_relay                           (GkdSshAgentCall *call);
-
 gboolean              gkd_ssh_agent_write_all                       (int fd,
                                                                      const guchar *buf,
                                                                      int len,
diff --git a/daemon/ssh-agent/gkd-ssh-agent-process.c b/daemon/ssh-agent/gkd-ssh-agent-process.c
new file mode 100644
index 0000000..268b87f
--- /dev/null
+++ b/daemon/ssh-agent/gkd-ssh-agent-process.c
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2014 Stef Walter
+ *
+ * Gnome keyring is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * Gnome keyring is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Author: Stef Walter <stef thewalter net>
+ */
+
+#include "config.h"
+
+#include "gkd-ssh-agent-process.h"
+#include "gkd-ssh-agent-private.h"
+
+#include "daemon/gkd-util.h"
+
+#include <glib-unix.h>
+#include <glib/gstdio.h>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <errno.h>
+#include <unistd.h>
+
+enum {
+       PROP_0,
+       PROP_PATH
+};
+
+struct _GkdSshAgentProcess
+{
+       GObject object;
+       gchar *path;
+       gint socket_fd;   /* socket opened by the ssh-agent process */
+       gint output_fd;   /* stdout of the ssh-agent process */
+       GHashTable *keys; /* keys actually known to the ssh-agent process */
+       GMutex lock;
+       GPid pid;
+       guint output_id;
+       guint child_id;
+       gboolean ready;
+};
+
+G_DEFINE_TYPE (GkdSshAgentProcess, gkd_ssh_agent_process, G_TYPE_OBJECT);
+
+static void
+gkd_ssh_agent_process_init (GkdSshAgentProcess *self)
+{
+       self->socket_fd = -1;
+       self->output_fd = -1;
+       self->keys = g_hash_table_new_full (g_bytes_hash, g_bytes_equal,
+                                           (GDestroyNotify)g_bytes_unref, NULL);
+       g_mutex_init (&self->lock);
+}
+
+static void
+gkd_ssh_agent_process_finalize (GObject *object)
+{
+       GkdSshAgentProcess *self = GKD_SSH_AGENT_PROCESS (object);
+
+       if (self->socket_fd != -1)
+               close (self->socket_fd);
+       if (self->output_fd != -1)
+               close (self->output_fd);
+       if (self->output_id)
+               g_source_remove (self->output_id);
+       if (self->child_id)
+               g_source_remove (self->child_id);
+       if (self->pid)
+               kill (self->pid, SIGTERM);
+       if (self->keys)
+               g_hash_table_unref (self->keys);
+       g_unlink (self->path);
+       g_free (self->path);
+       g_mutex_clear (&self->lock);
+
+       G_OBJECT_CLASS (gkd_ssh_agent_process_parent_class)->finalize (object);
+}
+
+static void
+gkd_ssh_agent_process_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+       GkdSshAgentProcess *self = GKD_SSH_AGENT_PROCESS (object);
+
+       switch (prop_id) {
+       case PROP_PATH:
+               self->path = g_value_dup_string (value);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gkd_ssh_agent_process_class_init (GkdSshAgentProcessClass *klass)
+{
+       GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+       gobject_class->finalize = gkd_ssh_agent_process_finalize;
+       gobject_class->set_property = gkd_ssh_agent_process_set_property;
+       g_object_class_install_property (gobject_class, PROP_PATH,
+                                        g_param_spec_string ("path", "Path", "Path",
+                                                             "",
+                                                             G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE));
+}
+
+static void
+on_child_watch (GPid pid,
+                gint status,
+                gpointer user_data)
+{
+       GkdSshAgentProcess *self = GKD_SSH_AGENT_PROCESS (user_data);
+       GError *error = NULL;
+
+       if (pid != self->pid)
+               return;
+
+       g_mutex_lock (&self->lock);
+
+       self->pid = 0;
+
+       if (!g_spawn_check_exit_status (status, &error)) {
+               g_message ("ssh-agent: %s", error->message);
+               g_error_free (error);
+       }
+
+       g_mutex_unlock (&self->lock);
+}
+
+static gboolean
+on_output_watch (gint fd,
+                GIOCondition condition,
+                gpointer user_data)
+{
+       GkdSshAgentProcess *self = GKD_SSH_AGENT_PROCESS (user_data);
+       guint8 buf[1024];
+       gssize len;
+
+       if (condition & G_IO_IN) {
+               self->ready = TRUE;
+
+               len = read (fd, buf, sizeof (buf));
+               if (len < 0) {
+                       if (errno != EAGAIN && errno != EINTR)
+                               g_message ("couldn't read from ssh-agent stdout: %m");
+                       condition |= G_IO_ERR;
+               } else if (len > 0) {
+                       gkd_ssh_agent_write_all (1, buf, len, "stdout");
+               }
+       }
+
+       if (condition & G_IO_HUP || condition & G_IO_ERR)
+               return FALSE;
+
+       return TRUE;
+}
+
+static gboolean
+agent_start_inlock (GkdSshAgentProcess *self)
+{
+       const gchar *argv[] = { SSH_AGENT, "-D", "-a", self->path, NULL };
+       GError *error = NULL;
+       GPid pid;
+
+       if (!g_spawn_async_with_pipes ("/", (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD,
+                                      NULL, NULL, &pid, NULL, &self->output_fd, NULL, &error)) {
+               g_warning ("couldn't run %s: %s", SSH_AGENT, error->message);
+               g_error_free (error);
+               return FALSE;
+       }
+
+       self->ready = FALSE;
+       self->output_id = g_unix_fd_add (self->output_fd,
+                                        G_IO_IN | G_IO_HUP | G_IO_ERR,
+                                        on_output_watch, self);
+
+       self->pid = pid;
+       self->child_id = g_child_watch_add (self->pid, on_child_watch, self);
+
+       return TRUE;
+}
+
+static gboolean
+on_timeout (gpointer user_data)
+{
+       gboolean *timedout = user_data;
+       *timedout = TRUE;
+       return TRUE;
+}
+
+gboolean
+gkd_ssh_agent_process_connect (GkdSshAgentProcess *self)
+{
+       gboolean started = FALSE;
+       struct sockaddr_un addr;
+       gboolean timedout = FALSE;
+       guint source;
+       gint sock;
+
+       g_mutex_lock (&self->lock);
+
+       if (self->pid == 0 || kill (self->pid, 0) != 0)
+               started = agent_start_inlock (self);
+
+       addr.sun_family = AF_UNIX;
+       g_strlcpy (addr.sun_path, self->path, sizeof (addr.sun_path));
+
+       if (started && !self->ready) {
+               source = g_timeout_add_seconds (5, on_timeout, &timedout);
+               while (!self->ready && !timedout)
+                       g_main_context_iteration (NULL, FALSE);
+               g_source_remove (source);
+       }
+
+       if (!self->ready)
+               return FALSE;
+
+       sock = socket (AF_UNIX, SOCK_STREAM, 0);
+       g_return_val_if_fail (sock >= 0, -1);
+
+       if (connect (sock, (struct sockaddr*) &addr, sizeof (addr)) < 0) {
+               g_message ("couldn't connect to ssh-agent socket at: %s: %s",
+                          addr.sun_path, g_strerror (errno));
+               close (sock);
+               sock = -1;
+       }
+
+       self->socket_fd = sock;
+
+       g_mutex_unlock (&self->lock);
+
+       return sock != -1;
+}
+
+gboolean
+gkd_ssh_agent_process_call (GkdSshAgentProcess *self,
+                            EggBuffer          *req,
+                            EggBuffer          *resp)
+{
+       return gkd_ssh_agent_write_packet (self->socket_fd, req) &&
+              gkd_ssh_agent_read_packet (self->socket_fd, resp);
+}
+
+gboolean
+gkd_ssh_agent_process_lookup_key (GkdSshAgentProcess *self,
+                                  GBytes             *key)
+{
+       gboolean ret;
+       g_mutex_lock (&self->lock);
+       ret = g_hash_table_contains (self->keys, key);
+       g_mutex_unlock (&self->lock);
+       return ret;
+}
+
+void
+gkd_ssh_agent_process_add_key (GkdSshAgentProcess *self,
+                               GBytes             *key)
+{
+       g_mutex_lock (&self->lock);
+       g_hash_table_add (self->keys, g_bytes_ref (key));
+       g_mutex_unlock (&self->lock);
+}
+
+void
+gkd_ssh_agent_process_remove_key (GkdSshAgentProcess *self,
+                                  GBytes             *key)
+{
+       g_mutex_lock (&self->lock);
+       g_hash_table_remove (self->keys, key);
+       g_mutex_lock (&self->lock);
+}
+
+void
+gkd_ssh_agent_process_clear_keys (GkdSshAgentProcess *self)
+{
+       g_mutex_lock (&self->lock);
+       g_hash_table_remove_all (self->keys);
+       g_mutex_unlock (&self->lock);
+}
+
+GkdSshAgentProcess *
+gkd_ssh_agent_process_get_default (void)
+{
+       static volatile gsize initialized = 0;
+       static GkdSshAgentProcess *instance = NULL;
+
+       if (g_once_init_enter (&initialized)) {
+               const gchar *directory = gkd_util_get_master_directory ();
+               gchar *path = g_build_filename (directory, "ssh-agent-real", NULL);
+               instance = g_object_new (GKD_TYPE_SSH_AGENT_PROCESS, "path", path, NULL);
+               g_free (path);
+               g_once_init_leave (&initialized, 1);
+       }
+
+       return instance;
+}
diff --git a/daemon/ssh-agent/gkd-ssh-agent-process.h b/daemon/ssh-agent/gkd-ssh-agent-process.h
new file mode 100644
index 0000000..cb24fda
--- /dev/null
+++ b/daemon/ssh-agent/gkd-ssh-agent-process.h
@@ -0,0 +1,47 @@
+/*
+ * gnome-keyring
+ *
+ * Copyright (C) 2014 Stef Walter
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * 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.
+ *
+ * Author: Stef Walter <stef thewalter net>
+ */
+
+#ifndef __GKD_SSH_AGENT_PROCESS_H__
+#define __GKD_SSH_AGENT_PROCESS_H__
+
+#include <glib-object.h>
+
+#include "egg/egg-buffer.h"
+
+#define GKD_TYPE_SSH_AGENT_PROCESS gkd_ssh_agent_process_get_type ()
+G_DECLARE_FINAL_TYPE(GkdSshAgentProcess, gkd_ssh_agent_process, GKD, SSH_AGENT_PROCESS, GObject)
+
+GkdSshAgentProcess *gkd_ssh_agent_process_get_default (void);
+gboolean            gkd_ssh_agent_process_connect     (GkdSshAgentProcess *self);
+gboolean            gkd_ssh_agent_process_call        (GkdSshAgentProcess *self,
+                                                       EggBuffer          *req,
+                                                       EggBuffer          *resp);
+gboolean            gkd_ssh_agent_process_lookup_key  (GkdSshAgentProcess *self,
+                                                       GBytes             *key);
+void                gkd_ssh_agent_process_add_key     (GkdSshAgentProcess *self,
+                                                       GBytes             *key);
+void                gkd_ssh_agent_process_remove_key  (GkdSshAgentProcess *self,
+                                                       GBytes             *key);
+void                gkd_ssh_agent_process_clear_keys  (GkdSshAgentProcess *self);
+
+#endif /* __GKD_SSH_AGENT_PROCESS_H__ */
diff --git a/daemon/ssh-agent/gkd-ssh-agent.c b/daemon/ssh-agent/gkd-ssh-agent.c
index 8fd3eb6..bae3449 100644
--- a/daemon/ssh-agent/gkd-ssh-agent.c
+++ b/daemon/ssh-agent/gkd-ssh-agent.c
@@ -138,11 +138,10 @@ gkd_ssh_agent_write_packet (gint fd,
        return gkd_ssh_agent_write_all (fd, buffer->buf, buffer->len, "client");
 }
 
-gboolean
-gkd_ssh_agent_relay (GkdSshAgentCall *call)
+static gboolean
+agent_relay (GkdSshAgentCall *call)
 {
-       return gkd_ssh_agent_write_packet (call->ssh_agent, call->req) &&
-              gkd_ssh_agent_read_packet (call->ssh_agent, call->resp);
+       return gkd_ssh_agent_process_call (call->process, call->req, call->resp);
 }
 
 static gpointer
@@ -164,8 +163,10 @@ run_client_thread (gpointer data)
        call.req = &req;
        call.resp = &resp;
 
-       call.ssh_agent = gkd_ssh_agent_client_connect ();
-       if (call.ssh_agent < 0)
+       call.process = gkd_ssh_agent_process_get_default ();
+       if (!call.process)
+               goto out;
+       if (!gkd_ssh_agent_process_connect (call.process))
                goto out;
 
        for (;;) {
@@ -184,7 +185,7 @@ run_client_thread (gpointer data)
                if (op >= GKD_SSH_OP_MAX || gkd_ssh_agent_operations[op])
                        func = gkd_ssh_agent_operations[op];
                else
-                       func = gkd_ssh_agent_relay;
+                       func = agent_relay;
                if (!func (&call))
                        break;
 
@@ -287,7 +288,7 @@ gkd_ssh_agent_shutdown (void)
        g_list_free (socket_clients);
        socket_clients = NULL;
 
-       gkd_ssh_agent_client_cleanup ();
+       g_object_unref (gkd_ssh_agent_process_get_default ());
 }
 
 int


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]