[epiphany/pgriffis/web-extension/runtime-send-message: 4/4] WIP: Implement messages replies to runtime.sendMessage()




commit f692ceec2b63b77c1c3519f17c11b5120bf1e138
Author: Patrick Griffis <pgriffis igalia com>
Date:   Sun May 29 12:18:55 2022 -0500

    WIP: Implement messages replies to runtime.sendMessage()
    
    This is a rough draft and currently causes a crash.

 .../web-process-extension/ephy-webextension-api.c  |   2 +-
 .../resources/js/webextensions-common.js           |  20 +++
 src/webextension/api/runtime.c                     |  47 ++++--
 src/webextension/ephy-web-extension-manager.c      | 171 +++++++++++++++++++--
 src/webextension/ephy-web-extension-manager.h      |   9 ++
 5 files changed, 221 insertions(+), 28 deletions(-)
---
diff --git a/embed/web-process-extension/ephy-webextension-api.c 
b/embed/web-process-extension/ephy-webextension-api.c
index 2448376f5..362848f9b 100644
--- a/embed/web-process-extension/ephy-webextension-api.c
+++ b/embed/web-process-extension/ephy-webextension-api.c
@@ -208,7 +208,7 @@ ephy_send_message (const char *function_name,
     return; /* Can't reject in this case. */
 
   if (!jsc_value_is_array (function_args) || !jsc_value_is_function (resolve_callback)) {
-    g_autoptr (JSCValue) ret = jsc_value_function_call (reject_callback, G_TYPE_STRING, "Invalid Arguments", 
G_TYPE_NONE);
+    g_autoptr (JSCValue) ret = jsc_value_function_call (reject_callback, G_TYPE_STRING, 
"ephy_send_message(): Invalid Arguments", G_TYPE_NONE);
     return;
   }
 
diff --git a/embed/web-process-extension/resources/js/webextensions-common.js 
b/embed/web-process-extension/resources/js/webextensions-common.js
index c8194fd26..8aaf54819 100644
--- a/embed/web-process-extension/resources/js/webextensions-common.js
+++ b/embed/web-process-extension/resources/js/webextensions-common.js
@@ -26,6 +26,26 @@ class EphyEventListener {
         for (const listener of this._listeners)
             listener.callback (data);
     }
+
+    _emit_with_reply (message, sender, message_guid) {
+        let handled = false;
+        const reply_callback = function (reply_message) {
+            ephy_message ('runtime._sendMessageReply', [message_guid, reply_message]);
+        };
+
+        for (const listener of this._listeners) {
+            const ret = listener.callback (message, sender, reply_callback);
+            if (typeof ret === 'object' && typeof ret.then === 'function') {
+                ret.then(x => { reply_callback(x); }).catch(x => { reply_callback(); })
+                handled = true;
+            } else if (ret === true) {
+                // We expect listener.callback to call `reply_callback`.
+                handled = true;
+            }
+        }
+
+        return handled;
+    }
 }
 
 const ephy_message = function (fn, args) {
diff --git a/src/webextension/api/runtime.c b/src/webextension/api/runtime.c
index e6295542d..1e2657342 100644
--- a/src/webextension/api/runtime.c
+++ b/src/webextension/api/runtime.c
@@ -58,14 +58,15 @@ is_empty_object (JSCValue *value)
   return FALSE;
 }
 
-static char *
-runtime_handler_send_message (EphyWebExtension  *self,
-                              char              *name,
-                              JSCValue          *args,
-                              const char        *context_guid,
-                              GError           **error)
+static void
+runtime_handler_send_message (EphyWebExtension *self,
+                              char             *name,
+                              JSCValue         *args,
+                              const char       *context_guid,
+                              GTask            *task)
 {
   EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();
+  g_autoptr (GError) error = NULL;
   g_autoptr (JSCValue) last_value = NULL;
   g_autoptr (JSCValue) message = NULL;
   g_autofree char *json = NULL;
@@ -75,22 +76,24 @@ runtime_handler_send_message (EphyWebExtension  *self,
   last_value = jsc_value_object_get_property_at_index (args, 2);
   if (!jsc_value_is_undefined (last_value)) {
     /* We don't actually support sending to external extensions yet. */
-    g_set_error_literal (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "extensionId is 
not supported");
-    return NULL;
+    error = g_error_new_literal (WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "extensionId is 
not supported");
+    g_task_return_error (task, g_steal_pointer (&error));
+    return;
   }
 
   last_value = jsc_value_object_get_property_at_index (args, 1);
   if (jsc_value_is_undefined (last_value) || jsc_value_is_null (last_value) || is_empty_object (last_value)) 
{
     message = jsc_value_object_get_property_at_index (args, 0);
   } else {
-    g_set_error_literal (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "extensionId is 
not supported");
-    return NULL;
+    error = g_error_new_literal (WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "extensionId is 
not supported");
+    g_task_return_error (task, g_steal_pointer (&error));
+    return;
   }
 
   json = jsc_value_to_json (message, 0);
-  ephy_web_extension_manager_emit_in_extension_views_except_self (manager, self, "runtime.onMessage", json, 
context_guid);
+  ephy_web_extension_manager_emit_in_extension_views_with_reply (manager, self, "runtime.onMessage", json, 
context_guid, "{}", task);
 
-  return NULL;
+  return;
 }
 
 static char *
@@ -120,12 +123,15 @@ runtime_handler_open_options_page (EphyWebExtension  *self,
   return NULL;
 }
 
-static EphyWebExtensionSyncApiHandler runtime_handlers[] = {
+static EphyWebExtensionSyncApiHandler runtime_sync_handlers[] = {
   {"getBrowserInfo", runtime_handler_get_browser_info},
-  {"sendMessage", runtime_handler_send_message},
   {"openOptionsPage", runtime_handler_open_options_page},
 };
 
+static EphyWebExtensionAsyncApiHandler runtime_async_handlers[] = {
+  {"sendMessage", runtime_handler_send_message},
+};
+
 void
 ephy_web_extension_api_runtime_handler (EphyWebExtension *self,
                                         char             *name,
@@ -136,8 +142,8 @@ ephy_web_extension_api_runtime_handler (EphyWebExtension *self,
   g_autoptr (GError) error = NULL;
   guint idx;
 
-  for (idx = 0; idx < G_N_ELEMENTS (runtime_handlers); idx++) {
-    EphyWebExtensionSyncApiHandler handler = runtime_handlers[idx];
+  for (idx = 0; idx < G_N_ELEMENTS (runtime_sync_handlers); idx++) {
+    EphyWebExtensionSyncApiHandler handler = runtime_sync_handlers[idx];
     char *ret;
 
     if (g_strcmp0 (handler.name, name) == 0) {
@@ -152,6 +158,15 @@ ephy_web_extension_api_runtime_handler (EphyWebExtension *self,
     }
   }
 
+  for (idx = 0; idx < G_N_ELEMENTS (runtime_async_handlers); idx++) {
+    EphyWebExtensionAsyncApiHandler handler = runtime_async_handlers[idx];
+
+    if (g_strcmp0 (handler.name, name) == 0) {
+      handler.execute (self, name, args, context_guid, task);
+      return;
+    }
+  }
+
   g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name);
   error = g_error_new_literal (WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "Not Implemented");
   g_task_return_error (task, g_steal_pointer (&error));
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
index ef31b12d4..a376ea003 100644
--- a/src/webextension/ephy-web-extension-manager.c
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -44,6 +44,8 @@
 
 #include <json-glib/json-glib.h>
 
+static void handle_message_reply (EphyWebExtension *web_extension, JSCValue *args);
+
 struct _EphyWebExtensionManager {
   GObject parent_instance;
 
@@ -54,6 +56,8 @@ struct _EphyWebExtensionManager {
 
   GHashTable *background_web_views;
   GHashTable *popup_web_views;
+
+  GHashTable *pending_messages;
 };
 
 G_DEFINE_TYPE (EphyWebExtensionManager, ephy_web_extension_manager, G_TYPE_OBJECT)
@@ -209,6 +213,7 @@ ephy_web_extension_manager_constructed (GObject *object)
   self->popup_web_views = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, 
(GDestroyNotify)g_ptr_array_free);
   self->page_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_hash_table_destroy);
   self->browser_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)destroy_widget_list);
+  self->pending_messages = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, 
(GDestroyNotify)g_hash_table_destroy);
   self->web_extensions = NULL;
 
   ephy_web_extension_manager_scan_directory (self, dir);
@@ -222,6 +227,7 @@ ephy_web_extension_manager_dispose (GObject *object)
   g_clear_pointer (&self->background_web_views, g_hash_table_destroy);
   g_clear_pointer (&self->popup_web_views, g_hash_table_destroy);
   g_clear_pointer (&self->page_action_map, g_hash_table_destroy);
+  g_clear_pointer (&self->pending_messages, g_hash_table_destroy);
   g_list_free_full (g_steal_pointer (&self->web_extensions), g_object_unref);
 }
 
@@ -516,7 +522,13 @@ ephy_web_extension_handle_user_message (WebKitWebContext  *context,
   js_context = jsc_context_new ();
   args = jsc_value_new_from_json (js_context, g_variant_get_string (webkit_user_message_get_parameters 
(message), NULL));
 
-  LOG ("%s(): Called for %s, function %s (%s)\n", __FUNCTION__, ephy_web_extension_get_name (web_extension), 
name, g_variant_get_string (webkit_user_message_get_parameters (message), NULL));
+  g_message ("%s(): Called for %s, function %s (%s)\n", __FUNCTION__, ephy_web_extension_get_name 
(web_extension), name, g_variant_get_string (webkit_user_message_get_parameters (message), NULL));
+
+  /* Private API for message replies handled by the manager. */
+  if (strcmp (name, "runtime._sendMessageReply") == 0) {
+    handle_message_reply (web_extension, args);
+    return TRUE;
+  }
 
   split = g_strsplit (name, ".", 2);
   if (g_strv_length (split) != 2) {
@@ -1147,6 +1159,45 @@ ephy_web_extension_manager_get_page_action (EphyWebExtensionManager *self,
   return ret;
 }
 
+static void
+handle_message_reply (EphyWebExtension *web_extension,
+                      JSCValue         *args)
+{
+  EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();
+  GHashTable *pending_messages = g_hash_table_lookup (manager->pending_messages, web_extension);
+  GTask *pending_task;
+  g_autofree char *message_guid = NULL;
+  g_autoptr (JSCValue) message_guid_value = NULL;
+  g_autoptr (JSCValue) reply_value = NULL;
+
+  g_message ("handle_message_reply");
+
+  message_guid_value = jsc_value_object_get_property_at_index (args, 0);
+  if (!jsc_value_is_string (message_guid_value)) {
+    g_debug ("Received invalid message reply");
+    return;
+  }
+
+  message_guid = jsc_value_to_string (message_guid_value);
+  pending_task = g_hash_table_lookup (pending_messages, message_guid);
+  if (!pending_task) {
+    g_debug ("Received message not found in pending replies");
+    return;
+  }
+
+  reply_value = jsc_value_object_get_property_at_index (args, 1);
+  g_task_return_pointer (pending_task, jsc_value_to_json (reply_value, 0), g_free);
+  g_object_ref (pending_task); /* Final unref in on_web_extension_api_handler_finish() */
+  g_hash_table_remove (pending_messages, message_guid);
+}
+
+typedef struct {
+  EphyWebExtension *web_extension;
+  const char *message_guid; /* Owned by manager->pending_messages. */
+  guint pending_view_responses;
+  gboolean handled;
+} PendingMessageReplyTracker;
+
 static const char *
 get_guid_for_view (WebKitWebView *view)
 {
@@ -1154,24 +1205,103 @@ get_guid_for_view (WebKitWebView *view)
   return g_object_get_data (G_OBJECT (context), "guid");
 }
 
+static void
+on_extension_emit_ready (GObject       *source,
+                         GAsyncResult  *result,
+                         gpointer       user_data)
+{
+  EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();
+  PendingMessageReplyTracker *tracker = user_data;
+  GHashTable *pending_messages;
+  g_autoptr (GError) error = NULL;
+  g_autoptr (WebKitJavascriptResult) js_result = NULL;
+  g_autoptr (JSCValue) js_value = NULL;
+  
+  js_result = webkit_web_view_run_javascript_finish (WEBKIT_WEB_VIEW (source),
+                                                     result,
+                                                     &error);
+
+  if (error) {
+    g_warning ("%s", error->message);
+  } else {
+    js_value = webkit_javascript_result_get_js_value (js_result);
+    if (jsc_value_to_boolean (js_value))
+      tracker->handled = TRUE;
+  }
+
+  /* Once all views have been notified it will either be handled by one of them, in which case 
+   * handle_message_reply() finishes the task, or we finish it here with an empty response. */
+  /* FIXME: A race condition is possible where a view is destroyed before it responds. */
+  tracker->pending_view_responses--;
+  if (tracker->pending_view_responses == 0) {
+    if (!tracker->handled) {
+      GTask *pending_task;
+      pending_messages = g_hash_table_lookup (manager->pending_messages, tracker->web_extension);
+      pending_task = g_hash_table_lookup (pending_messages, tracker->message_guid);
+      g_assert (pending_task);
+      g_task_return_pointer (pending_task, NULL, NULL);
+      g_object_ref (pending_task); /* Final unref in on_web_extension_api_handler_finish() */
+      g_hash_table_remove (pending_messages, tracker->message_guid);
+    }
+    g_free (tracker);
+  }
+}
+
 static void
 ephy_web_extension_manager_emit_in_extension_views_internal (EphyWebExtensionManager *self,
                                                              EphyWebExtension        *web_extension,
                                                              const char              *name,
-                                                             const char              *json,
-                                                             const char              *exception_guid)
+                                                             const char              *message_json,
+                                                             const char              *exception_guid,
+                                                             const char              *sender_json,
+                                                             GTask                   *reply_task)
 {
   WebKitWebView *background_view = ephy_web_extension_manager_get_background_web_view (self, web_extension);
   GPtrArray *popup_views = g_hash_table_lookup (self->popup_web_views, web_extension);
-  g_autofree char *script = g_strdup_printf ("window.browser.%s._emit(%s);", name, json);
+  g_autofree char *script = NULL;
+  PendingMessageReplyTracker *tracker = NULL;
+  guint pending_views = 0;
+
+  /* The `runtime.sendMessage()` API emits `runtime.onMessage` and waits for a reply.
+   * The way this is implemented is:
+   *  - All API handlers can be async: Returning a Promise backed by GTask (@reply_task).
+   *  - Instead of completing the GTask we store it for each message waiting on replies.
+   *  - We then call every extension view and track if any of them handled it (see webextensions-common.js).
+   *  - If none handled it we complete with an empty message.
+   *  - Otherwise we wait for our private `runtime._sendMessageReply` API call.
+   *  - The first `runtime._sendMessageReply` call wins and completes the GTask with its data.
+   */
+  if (reply_task) {
+    char *message_guid = g_dbus_generate_guid ();
+    GHashTable *pending_messages = g_hash_table_lookup (self->pending_messages, web_extension);
+  
+    tracker = g_new0 (PendingMessageReplyTracker, 1);
+    tracker->web_extension = web_extension;
+
+    if (!pending_messages) {
+      pending_messages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
(GDestroyNotify)g_object_unref);
+      g_hash_table_insert (self->pending_messages, web_extension, pending_messages);
+    }
+
+    script = g_strdup_printf ("window.browser.%s._emit_with_reply(%s, %s, '%s');", name, message_json, 
sender_json, message_guid);
+  
+    if (!g_hash_table_replace (pending_messages, message_guid, reply_task))
+      g_warning ("Duplicate message GUID");
+  }
+  else
+    script = g_strdup_printf ("window.browser.%s._emit(%s);", name, message_json);
+
+  g_message ("SCRIPT: %s", script);
 
   if (background_view) {
-    if (g_strcmp0 (get_guid_for_view (background_view), exception_guid) != 0)
+    if (g_strcmp0 (get_guid_for_view (background_view), exception_guid) != 0) {
       webkit_web_view_run_javascript (background_view,
                                       script,
                                       NULL,
-                                      NULL,
-                                      NULL);
+                                      on_extension_emit_ready,
+                                      tracker);
+      pending_views++;
+    }
   }
 
   if (popup_views) {
@@ -1183,10 +1313,14 @@ ephy_web_extension_manager_emit_in_extension_views_internal (EphyWebExtensionMan
       webkit_web_view_run_javascript (popup_view,
                                       script,
                                       NULL,
-                                      NULL,
-                                      NULL);
+                                      on_extension_emit_ready,
+                                      tracker);
+      pending_views++;
     }
   }
+
+  if (tracker)
+    tracker->pending_view_responses = pending_views;
 }
 
 void
@@ -1195,7 +1329,7 @@ ephy_web_extension_manager_emit_in_extension_views (EphyWebExtensionManager *sel
                                                     const char              *name,
                                                     const char              *json)
 {
-  ephy_web_extension_manager_emit_in_extension_views_internal (self, web_extension, name, json, NULL);
+  ephy_web_extension_manager_emit_in_extension_views_internal (self, web_extension, name, json, NULL, NULL, 
NULL);
 }
 
 void
@@ -1206,5 +1340,20 @@ ephy_web_extension_manager_emit_in_extension_views_except_self (EphyWebExtension
                                                                 const char              *context_guid)
 {
   g_assert (context_guid);
-  ephy_web_extension_manager_emit_in_extension_views_internal (self, web_extension, name, json, 
context_guid);
+  ephy_web_extension_manager_emit_in_extension_views_internal (self, web_extension, name, json, 
context_guid, NULL, NULL);
+}
+
+void
+ephy_web_extension_manager_emit_in_extension_views_with_reply (EphyWebExtensionManager *self,
+                                                               EphyWebExtension        *web_extension,
+                                                               const char              *name,
+                                                               const char              *json,
+                                                               const char              *context_guid,
+                                                               const char              *sender_json,
+                                                               GTask                   *reply_task)
+{
+  g_assert (context_guid);
+  g_assert (sender_json);
+  g_assert (reply_task);
+  ephy_web_extension_manager_emit_in_extension_views_internal (self, web_extension, name, json, 
context_guid, sender_json, reply_task);
 }
diff --git a/src/webextension/ephy-web-extension-manager.h b/src/webextension/ephy-web-extension-manager.h
index 70bbf7e07..ef54ba5da 100644
--- a/src/webextension/ephy-web-extension-manager.h
+++ b/src/webextension/ephy-web-extension-manager.h
@@ -81,4 +81,13 @@ void                     ephy_web_extension_manager_emit_in_extension_views_exce
                                                                                      const char              
*json,
                                                                                      const char              
*context_guid);
 
+void                     ephy_web_extension_manager_emit_in_extension_views_with_reply
+                                                                                    (EphyWebExtensionManager 
*self,
+                                                                                     EphyWebExtension        
*web_extension,
+                                                                                     const char              
*name,
+                                                                                     const char              
*json,
+                                                                                     const char              
*context_guid,
+                                                                                     const char              
*sender_json,
+                                                                                     GTask                   
*reply_task);
+
 G_END_DECLS


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