2003-04-27 Havoc Pennington <hp@pobox.com>
[platform/upstream/dbus.git] / bus / activation.c
index 9ae4422..3682eec 100644 (file)
@@ -28,6 +28,8 @@
 #include <dbus/dbus-internals.h>
 #include <dbus/dbus-hash.h>
 #include <dbus/dbus-list.h>
+#include <dbus/dbus-spawn.h>
+#include <dbus/dbus-timeout.h>
 #include <sys/types.h>
 #include <dirent.h>
 #include <errno.h>
@@ -43,6 +45,10 @@ struct BusActivation
   DBusHashTable *pending_activations;
   char *server_address;
   BusContext *context;
+  int n_pending_activations; /**< This is in fact the number of BusPendingActivationEntry,
+                              * i.e. number of pending activation requests, not pending
+                              * activations per se
+                              */
 };
 
 typedef struct
@@ -61,8 +67,14 @@ struct BusPendingActivationEntry
 
 typedef struct
 {
+  int refcount;
+  BusActivation *activation;
   char *service_name;
   DBusList *entries;
+  int n_entries;
+  DBusBabysitter *babysitter;
+  DBusTimeout *timeout;
+  unsigned int timeout_added : 1;
 } BusPendingActivation;
 
 static void
@@ -73,21 +85,66 @@ bus_pending_activation_entry_free (BusPendingActivationEntry *entry)
   
   if (entry->connection)
     dbus_connection_unref (entry->connection);
-
+  
   dbus_free (entry);
 }
 
 static void
-bus_pending_activation_free (BusPendingActivation *activation)
+handle_timeout_callback (DBusTimeout   *timeout,
+                         void          *data)
+{
+  BusPendingActivation *pending_activation = data;
+
+  while (!dbus_timeout_handle (pending_activation->timeout))
+    _dbus_wait_for_memory ();
+}
+
+static void
+bus_pending_activation_ref (BusPendingActivation *pending_activation)
+{
+  _dbus_assert (pending_activation->refcount > 0);
+  pending_activation->refcount += 1;
+}
+
+static void
+bus_pending_activation_unref (BusPendingActivation *pending_activation)
 {
   DBusList *link;
   
-  if (!activation)
+  if (pending_activation == NULL) /* hash table requires this */
     return;
 
-  dbus_free (activation->service_name);
+  _dbus_assert (pending_activation->refcount > 0);
+  pending_activation->refcount -= 1;
 
-  link = _dbus_list_get_first_link (&activation->entries);
+  if (pending_activation->refcount > 0)
+    return;
+  
+  if (pending_activation->timeout_added)
+    {
+      _dbus_loop_remove_timeout (bus_context_get_loop (pending_activation->activation->context),
+                                 pending_activation->timeout,
+                                 handle_timeout_callback, pending_activation);
+      pending_activation->timeout_added = FALSE;
+    }
+
+  if (pending_activation->timeout)
+    _dbus_timeout_unref (pending_activation->timeout);
+  
+  if (pending_activation->babysitter)
+    {
+      if (!_dbus_babysitter_set_watch_functions (pending_activation->babysitter,
+                                                 NULL, NULL, NULL,
+                                                 pending_activation->babysitter,
+                                                 NULL))
+        _dbus_assert_not_reached ("setting watch functions to NULL failed");
+      
+      _dbus_babysitter_unref (pending_activation->babysitter);
+    }
+  
+  dbus_free (pending_activation->service_name);
+
+  link = _dbus_list_get_first_link (&pending_activation->entries);
 
   while (link != NULL)
     {
@@ -95,11 +152,16 @@ bus_pending_activation_free (BusPendingActivation *activation)
 
       bus_pending_activation_entry_free (entry);
 
-      link = _dbus_list_get_next_link (&activation->entries, link);
+      link = _dbus_list_get_next_link (&pending_activation->entries, link);
     }
-  _dbus_list_clear (&activation->entries);
+  _dbus_list_clear (&pending_activation->entries);
+
+  pending_activation->activation->n_pending_activations -=
+    pending_activation->n_entries;
+
+  _dbus_assert (pending_activation->activation->n_pending_activations >= 0);
   
-  dbus_free (activation);
+  dbus_free (pending_activation);
 }
 
 static void
@@ -342,6 +404,7 @@ bus_activation_new (BusContext        *context,
   
   activation->refcount = 1;
   activation->context = context;
+  activation->n_pending_activations = 0;
   
   if (!_dbus_string_copy_data (address, &activation->server_address))
     {
@@ -358,7 +421,7 @@ bus_activation_new (BusContext        *context,
     }
 
   activation->pending_activations = _dbus_hash_table_new (DBUS_HASH_STRING, NULL,
-                                                         (DBusFreeFunction)bus_pending_activation_free);
+                                                         (DBusFreeFunction)bus_pending_activation_unref);
 
   if (activation->pending_activations == NULL)
     {
@@ -428,6 +491,75 @@ child_setup (void *data)
     }
 }
 
+typedef struct
+{
+  BusPendingActivation *pending_activation;
+  DBusPreallocatedHash *hash_entry;
+} RestorePendingData;
+
+static void
+restore_pending (void *data)
+{
+  RestorePendingData *d = data;
+
+  _dbus_assert (d->pending_activation != NULL);
+  _dbus_assert (d->hash_entry != NULL);
+
+  _dbus_verbose ("Restoring pending activation for service %s, has timeout = %d\n",
+                 d->pending_activation->service_name,
+                 d->pending_activation->timeout_added);
+  
+  _dbus_hash_table_insert_string_preallocated (d->pending_activation->activation->pending_activations,
+                                               d->hash_entry,
+                                               d->pending_activation->service_name, d->pending_activation);
+
+  bus_pending_activation_ref (d->pending_activation);
+  
+  d->hash_entry = NULL;
+}
+
+static void
+free_pending_restore_data (void *data)
+{
+  RestorePendingData *d = data;
+
+  if (d->hash_entry)
+    _dbus_hash_table_free_preallocated_entry (d->pending_activation->activation->pending_activations,
+                                              d->hash_entry);
+
+  bus_pending_activation_unref (d->pending_activation);
+  
+  dbus_free (d);
+}
+
+static dbus_bool_t
+add_restore_pending_to_transaction (BusTransaction       *transaction,
+                                    BusPendingActivation *pending_activation)
+{
+  RestorePendingData *d;
+
+  d = dbus_new (RestorePendingData, 1);
+  if (d == NULL)
+    return FALSE;
+  
+  d->pending_activation = pending_activation;
+  d->hash_entry = _dbus_hash_table_preallocate_entry (d->pending_activation->activation->pending_activations);
+  
+  bus_pending_activation_ref (d->pending_activation);
+  
+  if (d->hash_entry == NULL ||
+      !bus_transaction_add_cancel_hook (transaction, restore_pending, d,
+                                        free_pending_restore_data))
+    {
+      free_pending_restore_data (d);
+      return FALSE;
+    }
+
+  _dbus_verbose ("Saved pending activation to be restored if the transaction fails\n");
+  
+  return TRUE;
+}
+
 dbus_bool_t
 bus_activation_service_created (BusActivation  *activation,
                                const char     *service_name,
@@ -461,8 +593,7 @@ bus_activation_service_created (BusActivation  *activation,
              goto error;
            }
 
-         if (!dbus_message_set_sender (message, DBUS_SERVICE_DBUS) ||
-              !dbus_message_append_args (message,
+         if (!dbus_message_append_args (message,
                                         DBUS_TYPE_UINT32, DBUS_ACTIVATION_REPLY_ACTIVATED,
                                         0))
            {
@@ -471,29 +602,227 @@ bus_activation_service_created (BusActivation  *activation,
              goto error;
            }
           
-         if (!bus_transaction_send_message (transaction, entry->connection, message))
+         if (!bus_transaction_send_from_driver (transaction, entry->connection, message))
            {
              dbus_message_unref (message);
              BUS_SET_OOM (error);
              goto error;
            }
+          
+          dbus_message_unref (message);
        }
 
-      bus_pending_activation_entry_free (entry);
-      
-      _dbus_list_remove_link (&pending_activation->entries, link);      
       link = next;
     }
+
+  if (!add_restore_pending_to_transaction (transaction, pending_activation))
+    {
+      _dbus_verbose ("Could not add cancel hook to transaction to revert removing pending activation\n");
+      BUS_SET_OOM (error);
+      goto error;
+    }
   
   _dbus_hash_table_remove_string (activation->pending_activations, service_name);
 
   return TRUE;
 
  error:
-  _dbus_hash_table_remove_string (activation->pending_activations, service_name);
   return FALSE;
 }
 
+/**
+ * FIXME @todo the error messages here would ideally be preallocated
+ * so we don't need to allocate memory to send them.
+ * Using the usual tactic, prealloc an OOM message, then
+ * if we can't alloc the real error send the OOM error instead.
+ */
+static dbus_bool_t
+try_send_activation_failure (BusPendingActivation *pending_activation,
+                             const DBusError      *how)
+{
+  BusActivation *activation;
+  DBusList *link;
+  BusTransaction *transaction;
+  
+  activation = pending_activation->activation;
+
+  transaction = bus_transaction_new (activation->context);
+  if (transaction == NULL)
+    return FALSE;
+  
+  link = _dbus_list_get_first_link (&pending_activation->entries);
+  while (link != NULL)
+    {
+      BusPendingActivationEntry *entry = link->data;
+      DBusList *next = _dbus_list_get_next_link (&pending_activation->entries, link);
+      
+      if (dbus_connection_get_is_connected (entry->connection))
+       {
+          if (!bus_transaction_send_error_reply (transaction,
+                                                 entry->connection,
+                                                 how,
+                                                 entry->activation_message))
+            goto error;
+       }
+      
+      link = next;
+    }
+
+  bus_transaction_execute_and_free (transaction);
+  
+  return TRUE;
+
+ error:
+  if (transaction)
+    bus_transaction_cancel_and_free (transaction);
+  return FALSE;
+}
+
+/**
+ * Free the pending activation and send an error message to all the
+ * connections that were waiting for it.
+ */
+static void
+pending_activation_failed (BusPendingActivation *pending_activation,
+                           const DBusError      *how)
+{
+  /* FIXME use preallocated OOM messages instead of bus_wait_for_memory() */
+  while (!try_send_activation_failure (pending_activation, how))
+    _dbus_wait_for_memory ();
+
+  /* Destroy this pending activation */
+  _dbus_hash_table_remove_string (pending_activation->activation->pending_activations,
+                                  pending_activation->service_name);
+}
+
+static dbus_bool_t
+babysitter_watch_callback (DBusWatch     *watch,
+                           unsigned int   condition,
+                           void          *data)
+{
+  BusPendingActivation *pending_activation = data;
+  dbus_bool_t retval;
+  DBusBabysitter *babysitter;
+
+  babysitter = pending_activation->babysitter;
+  
+  _dbus_babysitter_ref (babysitter);
+  
+  retval = dbus_watch_handle (watch, condition);
+
+  /* FIXME this is broken in the same way that
+   * connection watches used to be; there should be
+   * a separate callback for status change, instead
+   * of doing "if we handled a watch status might
+   * have changed"
+   *
+   * Fixing this lets us move dbus_watch_handle
+   * calls into dbus-mainloop.c
+   */
+  
+  if (_dbus_babysitter_get_child_exited (babysitter))
+    {
+      DBusError error;
+
+      dbus_error_init (&error);
+      _dbus_babysitter_set_child_exit_error (babysitter, &error);
+
+      /* Destroys the pending activation */
+      pending_activation_failed (pending_activation, &error);
+
+      dbus_error_free (&error);
+    }
+  
+  _dbus_babysitter_unref (babysitter);
+
+  return retval;
+}
+
+static dbus_bool_t
+add_babysitter_watch (DBusWatch      *watch,
+                      void           *data)
+{
+  BusPendingActivation *pending_activation = data;
+
+  return _dbus_loop_add_watch (bus_context_get_loop (pending_activation->activation->context),
+                               watch, babysitter_watch_callback, pending_activation,
+                               NULL);
+}
+
+static void
+remove_babysitter_watch (DBusWatch      *watch,
+                         void           *data)
+{
+  BusPendingActivation *pending_activation = data;
+  
+  _dbus_loop_remove_watch (bus_context_get_loop (pending_activation->activation->context),
+                           watch, babysitter_watch_callback, pending_activation);
+}
+
+static dbus_bool_t
+pending_activation_timed_out (void *data)
+{
+  BusPendingActivation *pending_activation = data;
+  DBusError error;
+  
+  /* Kill the spawned process, since it sucks
+   * (not sure this is what we want to do, but
+   * may as well try it for now)
+   */
+  _dbus_babysitter_kill_child (pending_activation->babysitter);
+
+  dbus_error_init (&error);
+
+  dbus_set_error (&error, DBUS_ERROR_TIMED_OUT,
+                  "Activation of %s timed out",
+                  pending_activation->service_name);
+
+  pending_activation_failed (pending_activation, &error);
+
+  dbus_error_free (&error);
+
+  return TRUE;
+}
+
+static void
+cancel_pending (void *data)
+{
+  BusPendingActivation *pending_activation = data;
+
+  _dbus_verbose ("Canceling pending activation of %s\n",
+                 pending_activation->service_name);
+
+  if (pending_activation->babysitter)
+    _dbus_babysitter_kill_child (pending_activation->babysitter);
+  
+  _dbus_hash_table_remove_string (pending_activation->activation->pending_activations,
+                                  pending_activation->service_name);
+}
+
+static void
+free_pending_cancel_data (void *data)
+{
+  BusPendingActivation *pending_activation = data;
+  
+  bus_pending_activation_unref (pending_activation);
+}
+
+static dbus_bool_t
+add_cancel_pending_to_transaction (BusTransaction       *transaction,
+                                   BusPendingActivation *pending_activation)
+{  
+  if (!bus_transaction_add_cancel_hook (transaction, cancel_pending,
+                                        pending_activation,
+                                        free_pending_cancel_data))
+    return FALSE;
+
+  bus_pending_activation_ref (pending_activation); 
+  
+  _dbus_verbose ("Saved pending activation to be canceled if the transaction fails\n");
+  
+  return TRUE;
+}
+
 dbus_bool_t
 bus_activation_activate_service (BusActivation  *activation,
                                 DBusConnection *connection,
@@ -511,6 +840,15 @@ bus_activation_activate_service (BusActivation  *activation,
   dbus_bool_t retval;
 
   _DBUS_ASSERT_ERROR_IS_CLEAR (error);
+
+  if (activation->n_pending_activations >=
+      bus_context_get_max_pending_activations (activation->context))
+    {
+      dbus_set_error (error, DBUS_ERROR_LIMITS_EXCEEDED,
+                     "The maximum number of pending activations has been reached, activation of %s failed",
+                     service_name);
+      return FALSE;
+    }
   
   entry = _dbus_hash_table_lookup_string (activation->entries, service_name);
 
@@ -526,28 +864,34 @@ bus_activation_activate_service (BusActivation  *activation,
   _dbus_string_init_const (&service_str, service_name);
   if (bus_registry_lookup (bus_context_get_registry (activation->context), &service_str) != NULL)
     {
+      _dbus_verbose ("Service \"%s\" is already active\n", service_name);
+      
       message = dbus_message_new_reply (activation_message);
 
       if (!message)
        {
+          _dbus_verbose ("No memory to create reply to activate message\n");
          BUS_SET_OOM (error);
          return FALSE;
        }
 
-      if (!dbus_message_set_sender (message, DBUS_SERVICE_DBUS) ||
-          !dbus_message_append_args (message,
+      if (!dbus_message_append_args (message,
                                     DBUS_TYPE_UINT32, DBUS_ACTIVATION_REPLY_ALREADY_ACTIVE, 
                                     0))
        {
+          _dbus_verbose ("No memory to set args of reply to activate message\n");
          BUS_SET_OOM (error);
          dbus_message_unref (message);
          return FALSE;
        }
 
-      retval = bus_transaction_send_message (transaction, connection, message);
+      retval = bus_transaction_send_from_driver (transaction, connection, message);
       dbus_message_unref (message);
       if (!retval)
-       BUS_SET_OOM (error);
+        {
+          _dbus_verbose ("Failed to send reply\n");
+          BUS_SET_OOM (error);
+        }
 
       return retval;
     }
@@ -555,6 +899,7 @@ bus_activation_activate_service (BusActivation  *activation,
   pending_activation_entry = dbus_new0 (BusPendingActivationEntry, 1);
   if (!pending_activation_entry)
     {
+      _dbus_verbose ("Failed to create pending activation entry\n");
       BUS_SET_OOM (error);
       return FALSE;
     }
@@ -570,46 +915,106 @@ bus_activation_activate_service (BusActivation  *activation,
     {
       if (!_dbus_list_append (&pending_activation->entries, pending_activation_entry))
        {
+          _dbus_verbose ("Failed to append a new entry to pending activation\n");
+          
          BUS_SET_OOM (error);
          bus_pending_activation_entry_free (pending_activation_entry);
-
          return FALSE;
        }
+
+      pending_activation->n_entries += 1;
+      pending_activation->activation->n_pending_activations += 1;
     }
   else
     {
       pending_activation = dbus_new0 (BusPendingActivation, 1);
       if (!pending_activation)
        {
+          _dbus_verbose ("Failed to create pending activation\n");
+          
          BUS_SET_OOM (error);
          bus_pending_activation_entry_free (pending_activation_entry);   
          return FALSE;
        }
+
+      pending_activation->activation = activation;
+      pending_activation->refcount = 1;
+      
       pending_activation->service_name = _dbus_strdup (service_name);
       if (!pending_activation->service_name)
        {
+          _dbus_verbose ("Failed to copy service name for pending activation\n");
+          
+         BUS_SET_OOM (error);
+         bus_pending_activation_unref (pending_activation);
+         bus_pending_activation_entry_free (pending_activation_entry);   
+         return FALSE;
+       }
+
+      pending_activation->timeout =
+        _dbus_timeout_new (bus_context_get_activation_timeout (activation->context),
+                           pending_activation_timed_out,
+                           pending_activation,
+                           NULL);
+      if (!pending_activation->timeout)
+       {
+          _dbus_verbose ("Failed to create timeout for pending activation\n");
+          
+         BUS_SET_OOM (error);
+         bus_pending_activation_unref (pending_activation);
+         bus_pending_activation_entry_free (pending_activation_entry);   
+         return FALSE;
+       }
+
+      if (!_dbus_loop_add_timeout (bus_context_get_loop (activation->context),
+                                   pending_activation->timeout,
+                                   handle_timeout_callback,
+                                   pending_activation,
+                                   NULL))
+       {
+          _dbus_verbose ("Failed to add timeout for pending activation\n");
+          
          BUS_SET_OOM (error);
-         bus_pending_activation_free (pending_activation);
+         bus_pending_activation_unref (pending_activation);
          bus_pending_activation_entry_free (pending_activation_entry);   
          return FALSE;
        }
 
+      pending_activation->timeout_added = TRUE;
+      
       if (!_dbus_list_append (&pending_activation->entries, pending_activation_entry))
        {
+          _dbus_verbose ("Failed to add entry to just-created pending activation\n");
+          
          BUS_SET_OOM (error);
-         bus_pending_activation_free (pending_activation);
+         bus_pending_activation_unref (pending_activation);
          bus_pending_activation_entry_free (pending_activation_entry);   
          return FALSE;
        }
+
+      pending_activation->n_entries += 1;
+      pending_activation->activation->n_pending_activations += 1;
       
       if (!_dbus_hash_table_insert_string (activation->pending_activations,
-                                          pending_activation->service_name, pending_activation))
+                                          pending_activation->service_name,
+                                           pending_activation))
        {
+          _dbus_verbose ("Failed to put pending activation in hash table\n");
+          
          BUS_SET_OOM (error);
-         bus_pending_activation_free (pending_activation);
+         bus_pending_activation_unref (pending_activation);
          return FALSE;
        }
     }
+
+  if (!add_cancel_pending_to_transaction (transaction, pending_activation))
+    {
+      _dbus_verbose ("Failed to add pending activation cancel hook to transaction\n");
+      BUS_SET_OOM (error);
+      _dbus_hash_table_remove_string (activation->pending_activations,
+                                     pending_activation->service_name);
+      return FALSE;
+    }
   
   /* FIXME we need to support a full command line, not just a single
    * argv[0]
@@ -619,12 +1024,26 @@ bus_activation_activate_service (BusActivation  *activation,
   argv[0] = entry->exec;
   argv[1] = NULL;
 
-  if (!_dbus_spawn_async_with_babysitter (NULL, argv,
+  if (!_dbus_spawn_async_with_babysitter (&pending_activation->babysitter, argv,
                                           child_setup, activation, 
                                           error))
     {
-      _dbus_hash_table_remove_string (activation->pending_activations,
-                                     pending_activation->service_name);
+      _dbus_verbose ("Failed to spawn child\n");
+      _DBUS_ASSERT_ERROR_IS_SET (error);
+      return FALSE;
+    }
+
+  _dbus_assert (pending_activation->babysitter != NULL);
+  
+  if (!_dbus_babysitter_set_watch_functions (pending_activation->babysitter,
+                                             add_babysitter_watch,
+                                             remove_babysitter_watch,
+                                             NULL,
+                                             pending_activation,
+                                             NULL))
+    {
+      BUS_SET_OOM (error);
+      _dbus_verbose ("Failed to set babysitter watch functions\n");
       return FALSE;
     }