[gjs/ewlsh/remote-debugging] Add remote debugger




commit 22d3293f425630cda4949d67c2fecb4814cc34a6
Author: Evan Welsh <contact evanwelsh com>
Date:   Sat Jul 31 19:46:46 2021 -0700

    Add remote debugger

 gjs/console.cpp                             |   5 +
 gjs/context.h                               |   3 +
 gjs/debugger.cpp                            |  42 ++
 gjs/global.h                                |   3 +-
 gjs/remote-server.cpp                       | 169 +++++++
 gjs/remote-server.h                         |  66 +++
 gjs/socket-connection.cpp                   | 227 +++++++++
 gjs/socket-connection.h                     |  92 ++++
 gjs/socket-monitor.cpp                      |  43 ++
 gjs/socket-monitor.h                        |  32 ++
 js.gresource.xml                            |   3 +-
 meson.build                                 |   3 +
 modules/script/_bootstrap/remoteDebugger.js | 714 ++++++++++++++++++++++++++++
 translate.js                                |  43 ++
 14 files changed, 1443 insertions(+), 2 deletions(-)
---
diff --git a/gjs/console.cpp b/gjs/console.cpp
index 49c82299..9ba6562c 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -30,6 +30,7 @@ static char *command = NULL;
 static gboolean print_version = false;
 static gboolean print_js_version = false;
 static gboolean debugging = false;
+static gboolean remote_debugging = false;
 static gboolean exec_as_module = false;
 static bool enable_profiler = false;
 
@@ -50,6 +51,7 @@ static GOptionEntry entries[] = {
         "Enable the profiler and write output to FILE (default: gjs-$PID.syscap)",
         "FILE" },
     { "debugger", 'd', 0, G_OPTION_ARG_NONE, &debugging, "Start in debug mode" },
+    { "remote debugger", 'D', 0, G_OPTION_ARG_NONE, &remote_debugging, "Start in remote debug mode" },
     { NULL }
 };
 // clang-format on
@@ -381,6 +383,9 @@ main(int argc, char **argv)
     if (debugging)
         gjs_context_setup_debugger_console(js_context);
 
+    else if (remote_debugging)
+        gjs_context_setup_remote_debugger_console(js_context);
+
     int code = define_argv_and_eval_script(js_context, script_argc, script_argv,
                                            script, len, filename);
 
diff --git a/gjs/context.h b/gjs/context.h
index d2ed9eb4..5dae19f0 100644
--- a/gjs/context.h
+++ b/gjs/context.h
@@ -101,6 +101,9 @@ GJS_EXPORT GJS_USE const char* gjs_get_js_version(void);
 GJS_EXPORT
 void gjs_context_setup_debugger_console(GjsContext* gjs);
 
+GJS_EXPORT
+void gjs_context_setup_remote_debugger_console(GjsContext* gjs);
+
 G_END_DECLS
 
 #endif /* GJS_CONTEXT_H_ */
diff --git a/gjs/debugger.cpp b/gjs/debugger.cpp
index 585024d3..f2c5be33 100644
--- a/gjs/debugger.cpp
+++ b/gjs/debugger.cpp
@@ -21,6 +21,7 @@
 #    endif
 #endif
 
+#include <gio/gio.h>
 #include <glib.h>
 
 #include <js/CallArgs.h>
@@ -38,6 +39,7 @@
 #include "gjs/jsapi-util-args.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/macros.h"
+#include "gjs/remote-server.h"
 
 GJS_JSAPI_RETURN_CONVENTION
 static bool quit(JSContext* cx, unsigned argc, JS::Value* vp) {
@@ -109,6 +111,12 @@ static JSFunctionSpec debugger_funcs[] = {
     JS_FN("readline", do_readline, 1, GJS_MODULE_PROP_FLAGS),
     JS_FS_END
 };
+
+static JSFunctionSpec remote_debugger_funcs[] = {
+    JS_FN("writeMessage", gjs_socket_connection_write_message, 2, GJS_MODULE_PROP_FLAGS),
+    JS_FN("startRemoteDebugging", gjs_start_remote_debugging, 1, GJS_MODULE_PROP_FLAGS),
+    JS_FS_END
+};
 // clang-format on
 
 void gjs_context_setup_debugger_console(GjsContext* gjs) {
@@ -135,3 +143,37 @@ void gjs_context_setup_debugger_console(GjsContext* gjs) {
                                       "debugger"))
         gjs_log_exception(cx);
 }
+
+void gjs_context_setup_remote_debugger_console(GjsContext* gjs) {
+    auto cx = static_cast<JSContext*>(gjs_context_get_native_context(gjs));
+
+    JS::RootedObject debuggee(cx, gjs_get_import_global(cx));
+    JS::RootedObject debugger_global(
+        cx, gjs_create_global_object(cx, GjsGlobalType::DEBUGGER));
+    {
+        // Enter realm of the debugger and initialize it with the debuggee
+        JSAutoRealm ar(cx, debugger_global);
+        auto debugging_server = new RemoteDebuggingServer(cx, debugger_global);
+
+        gjs_set_global_slot(debugger_global,
+                            GjsDebuggerGlobalSlot::REMOTE_SERVER,
+                            JS::PrivateValue(debugging_server));
+
+        JS::RootedObject debuggee_wrapper(cx, debuggee);
+        if (!JS_WrapObject(cx, &debuggee_wrapper)) {
+            gjs_log_exception(cx);
+            return;
+        }
+
+        const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+        JS::RootedValue v_wrapper(cx, JS::ObjectValue(*debuggee_wrapper));
+        if (!JS_SetPropertyById(cx, debugger_global, atoms.debuggee(),
+                                v_wrapper) ||
+            !JS_DefineFunctions(cx, debugger_global, debugger_funcs) ||
+            !JS_DefineFunctions(cx, debugger_global, remote_debugger_funcs) ||
+            !gjs_define_global_properties(cx, debugger_global,
+                                          GjsGlobalType::DEBUGGER,
+                                          "GJS debugger", "remoteDebugger"))
+            gjs_log_exception(cx);
+    }
+}
diff --git a/gjs/global.h b/gjs/global.h
index 569a8ce1..aeae6eba 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -32,7 +32,8 @@ enum class GjsBaseGlobalSlot : uint32_t {
 };
 
 enum class GjsDebuggerGlobalSlot : uint32_t {
-    LAST = static_cast<uint32_t>(GjsBaseGlobalSlot::LAST),
+    REMOTE_SERVER = static_cast<uint32_t>(GjsBaseGlobalSlot::LAST),
+    LAST,
 };
 
 enum class GjsGlobalSlot : uint32_t {
diff --git a/gjs/remote-server.cpp b/gjs/remote-server.cpp
new file mode 100644
index 00000000..39442f58
--- /dev/null
+++ b/gjs/remote-server.cpp
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017 Igalia S.L.
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include "gjs/remote-server.h"
+#include <gio/gio.h>
+#include "config.h"
+#include "gjs/socket-connection.h"
+#include "gjs/socket-monitor.h"
+
+#include <stdint.h>
+
+static void trace_remote_global(JSTracer* trc, void* data) {
+    auto* remote_server = static_cast<RemoteDebuggingServer*>(data);
+
+    remote_server->trace(trc);
+}
+
+RemoteDebuggingServer::RemoteDebuggingServer(JSContext* cx,
+                                             JS::HandleObject debug_global)
+    : m_connection_id(0) {
+    m_cx = cx;
+
+    m_debug_global = debug_global;
+
+    JS_AddExtraGCRootsTracer(m_cx, trace_remote_global, this);
+}
+
+RemoteDebuggingServer::~RemoteDebuggingServer() {
+    if (m_service)
+        g_signal_handlers_disconnect_matched(m_service, G_SIGNAL_MATCH_DATA, 0,
+                                             0, nullptr, nullptr, this);
+
+    JS_RemoveExtraGCRootsTracer(m_cx, trace_remote_global, this);
+
+    m_debug_global = nullptr;
+    m_cx = nullptr;
+}
+
+bool RemoteDebuggingServer::start(const char* address, unsigned port) {
+    m_service = g_socket_service_new();
+    g_signal_connect(m_service, "incoming",
+                     G_CALLBACK(incomingConnectionCallback), this);
+
+    GjsAutoUnref<GSocketAddress> socketAddress =
+        (g_inet_socket_address_new_from_string(address, port));
+    GError* error = nullptr;
+    if (!g_socket_listener_add_address(
+            G_SOCKET_LISTENER(m_service), socketAddress.get(),
+            G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_TCP, nullptr, nullptr,
+            &error)) {
+        g_warning("Failed to start remote debugging server on %s:%u: %s\n",
+                  address, port, error->message);
+        g_error_free(error);
+        return false;
+    }
+
+    return true;
+}
+
+void RemoteDebuggingServer::trace(JSTracer* trc) {
+    JS::TraceEdge(trc, &m_debug_global, "Debug Global");
+}
+
+void RemoteDebuggingServer::triggerReadCallback(int32_t connection_id,
+                                                std::string content) {
+    JSAutoRealm ar(m_cx, m_debug_global);
+    JS::RootedObject global(m_cx, m_debug_global);
+
+    JS::RootedValue ignore_rval(m_cx);
+    JS::RootedValueArray<2> args(m_cx);
+    args[0].setInt32(connection_id);
+    if (!gjs_string_from_utf8_n(m_cx, content.data(), content.size(), args[1]))
+        return;
+
+    if (!JS_CallFunctionName(m_cx, global, "onReadMessage", args,
+                             &ignore_rval)) {
+        gjs_log_exception_uncaught(m_cx);
+    }
+}
+
+void RemoteDebuggingServer::triggerConnectionCallback(int32_t connection_id) {
+    JSAutoRealm ar(m_cx, m_debug_global);
+    JS::RootedObject global(m_cx, m_debug_global);
+
+    JS::RootedValue ignore_rval(m_cx);
+    JS::RootedValueArray<1> args(m_cx);
+    args[0].setInt32(connection_id);
+
+    if (!JS_CallFunctionName(m_cx, global, "onConnection", args,
+                             &ignore_rval)) {
+        gjs_log_exception_uncaught(m_cx);
+    }
+}
+
+gboolean RemoteDebuggingServer::incomingConnectionCallback(
+    GSocketService*, GSocketConnection* connection, GObject*, void* user_data) {
+    auto* debuggingServer = static_cast<RemoteDebuggingServer*>(user_data);
+
+    debuggingServer->incomingConnection(connection);
+    return true;
+}
+
+void RemoteDebuggingServer::incomingConnection(GSocketConnection* connection) {
+    // Increment connection id...
+    m_connection_id++;
+
+    std::shared_ptr<SocketConnection> socket_connection =
+        SocketConnection::create(m_connection_id, connection, this);
+
+    int32_t id = socket_connection->id();
+    m_connections.insert_or_assign(id, std::move(socket_connection));
+
+    triggerConnectionCallback(id);
+}
+
+void RemoteDebuggingServer::connectionDidClose(
+    std::shared_ptr<SocketConnection> clientConnection) {
+    m_connections.erase(clientConnection->id());
+}
+
+bool RemoteDebuggingServer::sendMessage(int32_t connection_id,
+                                        const char* message,
+                                        size_t message_len) {
+    ConnectionMap::const_iterator connection =
+        m_connections.find(connection_id);
+    if (connection == m_connections.end())
+        return false;
+
+    connection->second->sendMessage(message, message_len);
+    return true;
+}
+
+bool gjs_socket_connection_write_message(JSContext* cx, unsigned argc,
+                                         JS::Value* vp) {
+    g_assert(gjs_global_is_type(cx, GjsGlobalType::DEBUGGER) &&
+             "Global is debugger");
+
+    auto server = static_cast<RemoteDebuggingServer*>(
+        gjs_get_global_slot(JS::CurrentGlobalOrNull(cx),
+                            GjsDebuggerGlobalSlot::REMOTE_SERVER)
+            .toPrivate());
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    int32_t connection_id;
+    JS::UniqueChars message;
+    if (!gjs_parse_call_args(cx, "writeMessage", args, "is", "connection_id",
+                             &connection_id, "message", &message))
+        return false;
+
+    server->sendMessage(connection_id, message.get(), strlen(message.get()));
+    return true;
+}
+
+bool gjs_start_remote_debugging(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    g_assert(gjs_global_is_type(cx, GjsGlobalType::DEBUGGER) &&
+             "Global is debugger");
+    auto server = static_cast<RemoteDebuggingServer*>(
+        gjs_get_global_slot(JS::CurrentGlobalOrNull(cx),
+                            GjsDebuggerGlobalSlot::REMOTE_SERVER)
+            .toPrivate());
+
+    uint32_t port;
+    if (!gjs_parse_call_args(cx, "start", args, "u", "port", &port))
+        return false;
+
+    return server->start("0.0.0.0", port);
+}
diff --git a/gjs/remote-server.h b/gjs/remote-server.h
new file mode 100644
index 00000000..56de6dda
--- /dev/null
+++ b/gjs/remote-server.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017 Igalia S.L.
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#ifndef GJS_REMOTE_SERVER_H_
+#define GJS_REMOTE_SERVER_H_
+
+#include <stdint.h>
+#include <algorithm>
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "gjs/socket-connection.h"
+#include "gjs/socket-monitor.h"
+
+typedef struct _GSocketConnection GSocketConnection;
+typedef struct _GSocketService GSocketService;
+
+class SocketConnection;
+
+using ConnectionMap =
+    std::unordered_map<int32_t, std::shared_ptr<SocketConnection>>;
+
+class RemoteDebuggingServer {
+    int32_t m_connection_id;
+
+ public:
+    RemoteDebuggingServer(JSContext* cx, JS::HandleObject debug_global);
+    ~RemoteDebuggingServer();
+
+    bool start(const char* address, unsigned port);
+
+ private:
+    static gboolean incomingConnectionCallback(GSocketService*,
+                                               GSocketConnection*, GObject*,
+                                               void*);
+    void incomingConnection(GSocketConnection* connection);
+
+    void connectionDidClose(std::shared_ptr<SocketConnection> clientConnection);
+
+    JSContext* m_cx;
+    GSocketService* m_service;
+    JS::Heap<JSObject*> m_debug_global;
+    ConnectionMap m_connections;
+
+ public:
+    void trace(JSTracer* trc);
+    void triggerReadCallback(int32_t connection_id, std::string content);
+    void triggerConnectionCallback(int32_t connection_id);
+    bool sendMessage(int32_t connection_id, const char* message,
+                     size_t message_len);
+    bool isRunning() const { return m_service != nullptr; }
+};
+
+bool gjs_socket_connection_on_read_message(JSContext* cx, unsigned argc,
+                                           JS::Value* vp);
+
+bool gjs_socket_connection_write_message(JSContext* cx, unsigned argc,
+                                         JS::Value* vp);
+bool gjs_start_remote_debugging(JSContext* cx, unsigned argc, JS::Value* vp);
+
+bool gjs_socket_connection_on_connection(JSContext* cx, unsigned argc,
+                                         JS::Value* vp);
+#endif
\ No newline at end of file
diff --git a/gjs/socket-connection.cpp b/gjs/socket-connection.cpp
new file mode 100644
index 00000000..3b0d31eb
--- /dev/null
+++ b/gjs/socket-connection.cpp
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2019 Igalia, S.L.
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include <config.h>  // for HAVE_READLINE_READLINE_H, HAVE_UNISTD_H
+
+#include <stdint.h>
+#include <stdio.h>  // for feof, fflush, fgets, stdin, stdout
+
+#ifdef HAVE_READLINE_READLINE_H
+#    include <readline/history.h>
+#    include <readline/readline.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+#    include <unistd.h>  // for isatty, STDIN_FILENO
+#elif defined(_WIN32)
+#    include <io.h>
+#    ifndef STDIN_FILENO
+#        define STDIN_FILENO 0
+#    endif
+#endif
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <js/CallArgs.h>
+#include <js/PropertySpec.h>
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <js/Utility.h>  // for UniqueChars
+#include <js/Value.h>
+#include <jsapi.h>  // for JS_DefineFunctions, JS_NewStringCopyZ
+#include <functional>
+#include <map>
+#include <vector>
+
+#include "gjs/atoms.h"
+#include "gjs/context-private.h"
+#include "gjs/context.h"
+#include "gjs/global.h"
+#include "gjs/jsapi-util-args.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/macros.h"
+#include "gjs/remote-server.h"
+#include "gjs/socket-connection.h"
+
+static const unsigned defaultBufferSize = 4096;
+
+SocketConnection::SocketConnection(int32_t id, GSocketConnection* connection,
+                                   RemoteDebuggingServer* server)
+    : m_id(id), m_server(server), m_connection(g_object_ref(connection)) {
+    m_readBuffer.reserve(defaultBufferSize);
+    m_writeBuffer.reserve(defaultBufferSize);
+
+    GSocket* socket = g_socket_connection_get_socket(m_connection);
+    g_socket_set_blocking(socket, FALSE);
+
+    m_readMonitor.start(socket, G_IO_IN,
+                        [this](GIOCondition condition) -> bool {
+                            if (isClosed())
+                                return G_SOURCE_REMOVE;
+
+                            if (condition & G_IO_HUP || condition & G_IO_ERR ||
+                                condition & G_IO_NVAL) {
+                                didClose();
+                                return G_SOURCE_REMOVE;
+                            }
+
+                            g_assert(condition & G_IO_IN);
+                            return read();
+                        });
+}
+
+SocketConnection::~SocketConnection() {
+    m_server = nullptr;
+
+    g_clear_object(&m_connection);
+}
+
+bool SocketConnection::read() {
+    while (true) {
+        size_t previousBufferSize = m_readBuffer.size();
+        if (m_readBuffer.capacity() - previousBufferSize <= 0)
+            m_readBuffer.reserve(m_readBuffer.capacity() + defaultBufferSize);
+
+        GError* error = nullptr;
+        char bytes[defaultBufferSize];
+        auto bytesRead =
+            g_socket_receive(g_socket_connection_get_socket(m_connection),
+                             bytes, defaultBufferSize, nullptr, &error);
+
+        if (bytesRead == -1) {
+            if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                g_error_free(error);
+                m_readBuffer.shrink_to_fit();
+                break;
+            }
+
+            g_warning("Error reading from socket connection: %s\n",
+                      error->message);
+            g_error_free(error);
+            // didClose();
+            return G_SOURCE_CONTINUE;
+        }
+
+        if (!bytesRead) {
+            didClose();
+            return G_SOURCE_REMOVE;
+        }
+
+        std::move(bytes, bytes + bytesRead, std::back_inserter(m_readBuffer));
+
+        m_readBuffer.shrink_to_fit();
+
+        readMessage();
+        if (isClosed())
+            return G_SOURCE_REMOVE;
+    }
+    return G_SOURCE_CONTINUE;
+}
+
+bool SocketConnection::readMessage() {
+    if (m_readBuffer.size() == 0)
+        return false;
+
+    std::vector<char> contents_vector;
+
+    std::vector<char>::iterator i;
+
+    for (i = m_readBuffer.begin(); i != m_readBuffer.end(); i++) {
+        auto byte = *i;
+
+        contents_vector.push_back(byte);
+    }
+
+    std::string content =
+        std::string(contents_vector.begin(), contents_vector.end());
+
+    m_readBuffer.erase(m_readBuffer.begin(),
+                       m_readBuffer.begin() + contents_vector.size());
+    m_readBuffer.shrink_to_fit();
+
+    m_server->triggerReadCallback(m_id, content);
+
+    return true;
+}
+
+void SocketConnection::sendMessage(const char* bytes, size_t bytes_len) {
+    size_t previousBufferSize = m_writeBuffer.size();
+
+    m_writeBuffer.reserve(previousBufferSize + bytes_len);
+
+    std::move(bytes, bytes + bytes_len, std::back_inserter(m_writeBuffer));
+
+    write();
+}
+
+void SocketConnection::write() {
+    if (isClosed()) {
+        printf("write abort\n");
+        return;
+    }
+
+    GError* error = nullptr;
+    auto bytesWritten = g_socket_send(
+        g_socket_connection_get_socket(m_connection), m_writeBuffer.data(),
+        m_writeBuffer.size(), nullptr, &error);
+
+    if (bytesWritten == -1) {
+        if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+            waitForSocketWritability();
+            g_error_free(error);
+            return;
+        }
+
+        g_warning("Error sending message on socket connection: %s\n",
+                  error->message);
+        g_error_free(error);
+        didClose();
+        return;
+    }
+
+    m_writeBuffer.erase(m_writeBuffer.begin(),
+                        m_writeBuffer.begin() + bytesWritten);
+    m_writeBuffer.shrink_to_fit();
+
+    if (!m_writeBuffer.empty())
+        waitForSocketWritability();
+}
+
+void SocketConnection::waitForSocketWritability() {
+    if (m_writeMonitor.isActive())
+        return;
+
+    m_writeMonitor.start(
+        g_socket_connection_get_socket(m_connection), G_IO_OUT,
+        [this, protectedThis = this->ref()](GIOCondition condition) -> bool {
+            if (condition & G_IO_OUT) {
+                // We can't stop the monitor from this lambda,
+                // because stop destroys the lambda.
+                // TODO: Keep alive...
+                g_idle_add(
+                    [](void* user_data) -> gboolean {
+                        auto self =
+                            reinterpret_cast<SocketConnection*>(user_data);
+                        self->m_writeMonitor.stop();
+                        self->write();
+                        return false;
+                    },
+                    this);
+            }
+            return G_SOURCE_REMOVE;
+        });
+}
+
+void SocketConnection::close() {
+    m_readMonitor.stop();
+    m_writeMonitor.stop();
+    m_connection = nullptr;
+}
+
+void SocketConnection::didClose() {
+    if (isClosed())
+        return;
+
+    close();
+}
diff --git a/gjs/socket-connection.h b/gjs/socket-connection.h
new file mode 100644
index 00000000..a246903c
--- /dev/null
+++ b/gjs/socket-connection.h
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2019 Igalia, S.L.
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#ifndef GJS_SOCKET_CONNECTION_H_
+#define GJS_SOCKET_CONNECTION_H_
+
+#include <config.h>  // for HAVE_READLINE_READLINE_H, HAVE_UNISTD_H
+
+#include <stdint.h>
+#include <stdio.h>  // for feof, fflush, fgets, stdin, stdout
+
+#ifdef HAVE_READLINE_READLINE_H
+#    include <readline/history.h>
+#    include <readline/readline.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+#    include <unistd.h>  // for isatty, STDIN_FILENO
+#elif defined(_WIN32)
+#    include <io.h>
+#    ifndef STDIN_FILENO
+#        define STDIN_FILENO 0
+#    endif
+#endif
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <js/CallArgs.h>
+#include <js/PropertySpec.h>
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <js/Utility.h>  // for UniqueChars
+#include <js/Value.h>
+#include <jsapi.h>  // for JS_DefineFunctions, JS_NewStringCopyZ
+#include <functional>
+#include <map>
+#include <memory>
+#include <vector>
+#include "gjs/atoms.h"
+#include "gjs/context-private.h"
+#include "gjs/context.h"
+#include "gjs/global.h"
+#include "gjs/jsapi-util-args.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/macros.h"
+#include "gjs/socket-monitor.h"
+
+class RemoteDebuggingServer;
+
+class SocketConnection : std::enable_shared_from_this<SocketConnection> {
+ public:
+    typedef void (*MessageCallback)(SocketConnection&, const char*, size_t,
+                                    gpointer);
+    static std::shared_ptr<SocketConnection> create(
+        int32_t id, GSocketConnection* connection,
+        RemoteDebuggingServer* debug_server) {
+        return std::make_shared<SocketConnection>(id, connection, debug_server);
+    }
+    std::vector<std::shared_ptr<SocketConnection>> m_keep_alive;
+    ~SocketConnection();
+
+    int32_t id() { return m_id; }
+
+    void sendMessage(const char*, size_t);
+
+    bool isClosed() const { return !m_connection; }
+    void close();
+
+    std::shared_ptr<SocketConnection> ref() { return shared_from_this(); }
+
+ public:
+    SocketConnection(int32_t, GSocketConnection*, RemoteDebuggingServer*);
+
+ private:
+    static gboolean idle_stop(void* user_data);
+    bool read();
+    bool readMessage();
+    void write();
+    void waitForSocketWritability();
+    void didClose();
+
+    int32_t m_id;
+    RemoteDebuggingServer* m_server;
+    GSocketConnection* m_connection;
+    std::vector<char> m_readBuffer;
+    GSocketMonitor m_readMonitor;
+    std::vector<char> m_writeBuffer;
+    GSocketMonitor m_writeMonitor;
+};
+
+#endif  // GJS_SOCKET_CONNECTION_H_
\ No newline at end of file
diff --git a/gjs/socket-monitor.cpp b/gjs/socket-monitor.cpp
new file mode 100644
index 00000000..c12528d6
--- /dev/null
+++ b/gjs/socket-monitor.cpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2015 Igalia S.L.
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include "gjs/socket-monitor.h"
+
+GSocketMonitor::~GSocketMonitor() { stop(); }
+
+bool GSocketMonitor::socketSourceCallback(GSocket*, GIOCondition condition,
+                                          GSocketMonitor* monitor) {
+    if (monitor->m_cancellable &&
+        g_cancellable_is_cancelled(monitor->m_cancellable))
+        return G_SOURCE_REMOVE;
+    return monitor->m_callback(condition);
+}
+
+void GSocketMonitor::start(GSocket* socket, GIOCondition condition,
+                           std::function<bool(GIOCondition)>&& callback) {
+    m_cancellable = g_cancellable_new();
+    m_source = g_socket_create_source(socket, condition, m_cancellable);
+    g_source_set_name(m_source, "[gjs] Socket monitor");
+
+    m_callback = std::move(callback);
+    g_source_set_callback(m_source,
+                          reinterpret_cast<GSourceFunc>(socketSourceCallback),
+                          this, nullptr);
+    g_source_set_priority(m_source, G_PRIORITY_HIGH);
+    g_source_attach(m_source, nullptr);
+}
+
+void GSocketMonitor::stop() {
+    if (!m_source)
+        return;
+
+    g_cancellable_cancel(m_cancellable);
+    m_cancellable = nullptr;
+    g_source_destroy(m_source);
+    m_source = nullptr;
+    m_callback = nullptr;
+}
diff --git a/gjs/socket-monitor.h b/gjs/socket-monitor.h
new file mode 100644
index 00000000..e2c4385d
--- /dev/null
+++ b/gjs/socket-monitor.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2015 Igalia S.L.
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#ifndef GJS_SOCKET_MONITOR_H_
+#define GJS_SOCKET_MONITOR_H_
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <functional>
+#include "gjs/jsapi-util.h"
+
+typedef struct _GSocket GSocket;
+
+class GSocketMonitor {
+ public:
+    GSocketMonitor() = default;
+    ~GSocketMonitor();
+
+    void start(GSocket*, GIOCondition, std::function<bool(GIOCondition)>&&);
+    void stop();
+    bool isActive() const { return !!m_source; }
+
+ private:
+    static bool socketSourceCallback(GSocket*, GIOCondition, GSocketMonitor*);
+
+    GSource* m_source;
+    GCancellable* m_cancellable;
+    std::function<bool(GIOCondition)> m_callback;
+};
+
+#endif  // GJS_SOCKET_MONITOR_H_
\ No newline at end of file
diff --git a/js.gresource.xml b/js.gresource.xml
index 47be6425..9b080c14 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -9,13 +9,14 @@
 
     <!-- ESM-based modules -->
     <file>modules/esm/_bootstrap/default.js</file>
-  
+
     <file>modules/esm/cairo.js</file>
     <file>modules/esm/gettext.js</file>
     <file>modules/esm/gi.js</file>
     <file>modules/esm/system.js</file>
 
     <!-- Script-based Modules -->
+    <file>modules/script/_bootstrap/remoteDebugger.js</file>
     <file>modules/script/_bootstrap/debugger.js</file>
     <file>modules/script/_bootstrap/default.js</file>
     <file>modules/script/_bootstrap/coverage.js</file>
diff --git a/meson.build b/meson.build
index 9214a5fe..bf5b44d2 100644
--- a/meson.build
+++ b/meson.build
@@ -407,6 +407,9 @@ libgjs_sources = [
     'gjs/objectbox.cpp', 'gjs/objectbox.h',
     'gjs/profiler.cpp', 'gjs/profiler-private.h',
     'gjs/text-encoding.cpp', 'gjs/text-encoding.h',
+    'gjs/remote-server.cpp','gjs/remote-server.h', 
+    'gjs/socket-connection.cpp','gjs/socket-connection.h', 
+    'gjs/socket-monitor.cpp','gjs/socket-monitor.h', 
     'gjs/stack.cpp',
     'modules/console.cpp', 'modules/console.h',
     'modules/modules.cpp', 'modules/modules.h',
diff --git a/modules/script/_bootstrap/remoteDebugger.js b/modules/script/_bootstrap/remoteDebugger.js
new file mode 100644
index 00000000..4d2f16b2
--- /dev/null
+++ b/modules/script/_bootstrap/remoteDebugger.js
@@ -0,0 +1,714 @@
+/* global debuggee, quit, loadNative, readline, uneval, writeMessage */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+// @ts-check
+
+// @ts-expect-error
+const {print, logError} = loadNative('_print');
+/** @type {(msg: string) => void} */
+const debug = print;
+
+const Resources = {
+    TYPES: {
+        CONSOLE_MESSAGE: 'console-message',
+        CSS_CHANGE: 'css-change',
+        CSS_MESSAGE: 'css-message',
+        DOCUMENT_EVENT: 'document-event',
+        ERROR_MESSAGE: 'error-message',
+        PLATFORM_MESSAGE: 'platform-message',
+        NETWORK_EVENT: 'network-event',
+        STYLESHEET: 'stylesheet',
+        NETWORK_EVENT_STACKTRACE: 'network-event-stacktrace',
+        REFLOW: 'reflow',
+        SOURCE: 'source',
+        THREAD_STATE: 'thread-state',
+        SERVER_SENT_EVENT: 'server-sent-event',
+        WEBSOCKET: 'websocket',
+        // storage types
+        CACHE_STORAGE: 'Cache',
+        COOKIE: 'cookies',
+        INDEXED_DB: 'indexed-db',
+        LOCAL_STORAGE: 'local-storage',
+        SESSION_STORAGE: 'session-storage',
+    },
+};
+
+function connectionPrefix(connectionId) {
+    return ['gjs', `conn${connectionId}`];
+}
+
+/** @type {Map<string, number>} */
+const actorCount = new Map();
+const connectionPool = new Map();
+
+/**
+ * @param {string} baseIdentifier
+ * @returns {number}
+ */
+function getActorCount(baseIdentifier) {
+    const count = actorCount.get(baseIdentifier) ?? 1;
+
+    actorCount.set(baseIdentifier, count + 1);
+
+    return count;
+}
+
+/**
+ *
+ * @param {number} connectionId
+ * @param {string} actorType
+ */
+function createId(connectionId, actorType) {
+    const baseIdentifier = [...connectionPrefix(connectionId), actorType].join(
+        '.'
+    );
+
+    return `${baseIdentifier}${getActorCount(baseIdentifier)}`;
+}
+
+/** @type {Map<string, Actor>} */
+const actorMap = new Map();
+
+/**
+ * @param {Actor} actor
+ */
+function registerActor(actor) {
+    actorMap.set(actor.actorID, actor);
+}
+
+class Actor {
+    /**
+     * @param {number} connectionId
+     * @param {string} typeName
+     * @param {string} actorID
+     */
+    constructor(connectionId, typeName, actorID) {
+        this.connectionId = connectionId;
+        this.typeName = typeName;
+        this.actorID = actorID;
+
+        registerActor(this);
+    }
+
+    form() {
+        return {};
+    }
+
+    write(json) {
+        const bytes = JSON.stringify(json);
+        debug(bytes);
+        writeMessage(this.connectionId, `${bytes.length}:${bytes}`);
+    }
+}
+
+class GlobalActor extends Actor {
+    /**
+     * @param {number} connectionId
+     * @param {string} typeName
+     */
+    constructor(connectionId, typeName) {
+        super(
+            connectionId,
+            typeName,
+            createId(connectionId, `${typeName}Actor`)
+        );
+    }
+}
+
+class TargetActor extends Actor {
+    /**
+     * @param {number} connectionId
+     * @param {string} typeName
+     * @param {GlobalActor} parent
+     */
+    constructor(connectionId, typeName, parent) {
+        super(connectionId, typeName, childOf(parent.actorID, typeName));
+    }
+}
+
+class DeviceActor extends GlobalActor {
+    constructor(connectionId) {
+        super(connectionId, 'device');
+    }
+
+    getDescription() {
+        return {
+            value: {
+                // TODO: Create UUID
+                appid: '{ec8230f7-c20a-464f-9b0e-13a3a9397381}',
+                apptype: 'gjs',
+                vendor: 'GNOME',
+                brandName: 'gjs',
+                name: 'gjs',
+                // TODO: Figure out versioning.
+                version: '89.0.2',
+                platformversion: '89.0.2',
+                geckoversion: '89.0.2',
+                canDebugServiceWorkers: false,
+            },
+        };
+    }
+}
+
+function childOf(parentActorId, actorType) {
+    const id = [parentActorId, actorType].join('/');
+
+    return `${id}${getActorCount(id)}`;
+}
+
+/**
+ * Debugger.Source objects have a `url` property that exposes the value
+ * that was passed to SpiderMonkey, but unfortunately often SpiderMonkey
+ * sets a URL even in cases where it doesn't make sense, so we have to
+ * explicitly ignore the URL value in these contexts to keep things a bit
+ * more consistent.
+ *
+ * @param {Debugger.Source} source
+ *
+ * @returns {string | null}
+ */
+function getDebuggerSourceURL(source) {
+    const introType = source.introductionType;
+
+    // These are all the sources that are eval or eval-like, but may still have
+    // a URL set on the source, so we explicitly ignore the source URL for these.
+    if (
+        introType === 'injectedScript' ||
+        introType === 'eval' ||
+        introType === 'debugger eval' ||
+        introType === 'Function' ||
+        introType === 'javascriptURL' ||
+        introType === 'eventHandler' ||
+        introType === 'domTimer'
+    )
+        return null;
+
+
+    // if (source.url && !source.url.includes(":"))
+    //     return `resource:///unknown/${source.url}`;
+
+    return source.url;
+}
+
+class SourceActor extends TargetActor {
+    /**
+     * @param {number} connectionId
+     * @param {ProcessDescriptorActor} processDescriptorActor
+     * @param {Debugger.Source} debuggerSource
+     */
+    constructor(connectionId, processDescriptorActor, debuggerSource) {
+        super(connectionId, 'source', processDescriptorActor);
+
+        this._debuggerSource = debuggerSource;
+    }
+
+    getBreakpointPositions() {
+        return {
+            positions: [],
+        };
+    }
+
+    getBreakableLines() {
+        return {
+            lines: [],
+        };
+    }
+
+    source() {
+        return {
+            source: this._debuggerSource?.source?.text ?? null,
+        };
+    }
+
+    form() {
+        const source = this._debuggerSource;
+
+        let introductionType = source.introductionType;
+        if (
+            introductionType === 'srcScript' ||
+            introductionType === 'inlineScript' ||
+            introductionType === 'injectedScript'
+        ) {
+            // These three used to be one single type, so here we combine them all
+            // so that clients don't see any change in behavior.
+            introductionType = 'scriptElement';
+        }
+
+        return {
+            actor: this.actorID,
+            sourceMapBaseURL: null,
+            extensionName: null,
+            url: getDebuggerSourceURL(source),
+            isBlackBoxed: false,
+            introductionType,
+            sourceMapURL: source.sourceMapURL,
+        };
+    }
+}
+
+const STATES = {
+    //  Before ThreadActor.attach is called:
+    DETACHED: 'detached',
+    //  After the actor is destroyed:
+    EXITED: 'exited',
+
+    // States possible in between DETACHED AND EXITED:
+    // Default state, when the thread isn't paused,
+    RUNNING: 'running',
+    // When paused on any type of breakpoint, or, when the client requested an interrupt.
+    PAUSED: 'paused',
+};
+
+class ThreadActor extends TargetActor {
+    /**
+     * @param {number} connectionId
+     * @param {ProcessDescriptorActor} processTargetActor
+     */
+    constructor(connectionId, processTargetActor) {
+        super(
+            connectionId,
+            'thread',
+            processTargetActor.contentProcessTargetActor.contentProcessActor
+        );
+        this.processTargetActor = processTargetActor;
+
+        this._dbg = new Debugger();
+
+        this._dbg.addDebuggee(debuggee);
+
+        this._state = STATES.DETACHED;
+        this._sources = [...this._dbg.findScripts()];
+        this._sourceActors = this._sources.map(
+            s => new SourceActor(connectionId, processTargetActor, s)
+        );
+    }
+
+    get state() {
+        return this._state;
+    }
+
+    attach({
+        pauseOnExceptions,
+        ignoreCaughtExceptions,
+        shouldShowOverlay,
+        shouldIncludeSavedFrames,
+        shouldIncludeAsyncLiveFrames,
+        skipBreakpoints,
+        logEventBreakpoints,
+        observeAsmJS,
+        breakpoints,
+        eventBreakpoints,
+    }) {
+        if (this.state === STATES.EXITED) {
+            return {
+                error: 'exited',
+                message: 'threadActor has exited',
+            };
+        }
+
+        if (this.state !== STATES.DETACHED) {
+            return {
+                error: 'wrongState',
+                message: `Current state is ${this.state}`,
+            };
+        }
+
+        this._dbg.onNewScript = this._onNewScript.bind(this);
+
+        this._state = STATES.RUNNING;
+
+        return {
+            type: STATES.RUNNING,
+            actor: this.actorID,
+        };
+    }
+
+    _onNewScript(source) {
+        if (this._sources.includes(source))
+            return;
+        const actor = new SourceActor(
+            this.connectionId,
+            this.processTargetActor,
+            source
+        );
+        this._sources.push(source);
+        this._sourceActors.push(actor);
+
+        this.write({
+            type: 'newSource',
+            source: actor.form(),
+        });
+    }
+
+    pauseOnExceptions({pauseOnExceptions, ignoreCaughtExceptions}) {
+        debug(`pauseOnExceptions: ${pauseOnExceptions}`);
+        debug(`ignoreCaughtExceptions: ${ignoreCaughtExceptions}`);
+        return {};
+    }
+
+    sources() {
+        return {
+            sources: this._sourceActors.map(actor => actor.form()),
+        };
+    }
+
+    reconfigure({}) {
+        return {};
+    }
+
+    form() {
+        return {
+            threadActor: {
+                actor: this.actorID,
+            },
+        };
+    }
+}
+
+class ConsoleActor extends TargetActor {
+    /**
+     * @param {number} connectionId
+     * @param {ContentProcess} contentProcessActor
+     */
+    constructor(connectionId, contentProcessActor) {
+        super(connectionId, 'console', contentProcessActor);
+    }
+
+    getCachedMessages({messageTypes}) {
+        debug(JSON.stringify(messageTypes));
+        return {messages: []};
+    }
+
+    startListeners({listeners}) {
+        debug(JSON.stringify(listeners));
+        return {listeners: []};
+    }
+}
+
+class ContentProcess extends Actor {
+    /**
+     * @param {number} connectionId
+     */
+    constructor(connectionId) {
+        super(
+            connectionId,
+            'content-process',
+            createId(connectionId, 'content-process')
+        );
+
+        this.consoleActor = new ConsoleActor(connectionId, this);
+    }
+}
+
+class ContentProcessTargetActor extends TargetActor {
+    /**
+     *
+     * @param {number} connectionId
+     * @param {ProcessDescriptorActor} parentActor
+     */
+    constructor(connectionId, parentActor) {
+        const contentProcess = new ContentProcess(connectionId);
+
+        super(connectionId, 'contentProcessTarget', contentProcess);
+
+        this.contentProcessActor = contentProcess;
+        this.parentActor = parentActor;
+
+        this.watcherActor = new WatcherActor(
+            connectionId,
+            this.contentProcessActor
+        );
+        // this._actorID = childOf(this.actorID, "contentProcessTarget");
+    }
+
+    form() {
+        return {
+            processID: 0,
+            actor: this.actorID,
+            threadActor: this.parentActor.threadActor.actorID,
+            consoleActor: this.contentProcessActor.consoleActor.actorID,
+            remoteType: 'privilegedmozilla',
+            traits: {
+                networkMonitor: false,
+                supportsTopLevelTargetFlag: false,
+                noPauseOnThreadActorAttach: true,
+            },
+        };
+    }
+}
+
+class WatcherActor extends TargetActor {
+    /**
+     * @param {number} connectionId
+     * @param {ContentProcess} contentProcessActor
+     */
+    constructor(connectionId, contentProcessActor) {
+        super(connectionId, 'watcher', contentProcessActor);
+        this.connectionId;
+        this.processDescriptorActor = contentProcessActor;
+    }
+
+    form() {
+        return {
+            actor: this.actorID,
+            // The resources and target traits should be removed all at the same time since the
+            // client has generic ways to deal with all of them (See Bug 1680280).
+        };
+    }
+}
+
+class ProcessDescriptorActor extends GlobalActor {
+    constructor(connectionId) {
+        super(connectionId, 'processDescriptor');
+
+        this.contentProcessTargetActor = new ContentProcessTargetActor(
+            connectionId,
+            this
+        );
+
+        // ThreadActor awkwardly depends on contentProcessTargetActor
+        this.threadActor = new ThreadActor(connectionId, this);
+    }
+
+    getTarget() {
+        return {
+            process: this.contentProcessTargetActor.form(),
+        };
+    }
+
+    getWatcher() {
+        return this.contentProcessTargetActor.watcherActor.form();
+    }
+
+    form() {
+        return {
+            actor: this.actorID,
+            id: 0,
+            isParent: true,
+            traits: {
+                watcher: true,
+                supportsReloadDescriptor: false,
+            },
+        };
+    }
+}
+
+class PreferenceActor extends GlobalActor {
+    /**
+     * @param {number} connectionId
+     */
+    constructor(connectionId) {
+        super(connectionId, 'pref');
+    }
+
+    getBoolPref() {
+        return {
+            value: false,
+        };
+    }
+}
+
+class RootActor extends Actor {
+    /**
+     * @param connectionId
+     * @param {DeviceActor} deviceActor
+     * @param {PreferenceActor} preferenceActor
+     * @param {ProcessDescriptorActor} mainProcessActor
+     */
+    constructor(connectionId, deviceActor, preferenceActor, mainProcessActor) {
+        super(connectionId, 'rootActor', `root${connectionId}`);
+
+        this.deviceActor = deviceActor;
+        this.preferenceActor = preferenceActor;
+        this.mainProcessActor = mainProcessActor;
+    }
+
+    getProcess(options) {
+        debug(JSON.stringify(options));
+        if (options.id !== 0) {
+            return {
+                error: 'noSuchActor!',
+            };
+        }
+
+        return {
+            processDescriptor: this.mainProcessActor.form(),
+        };
+    }
+
+    listServiceWorkerRegistrations() {
+        return {
+            registrations: [],
+        };
+    }
+
+    listWorkers() {
+        return {
+            workers: [],
+        };
+    }
+
+    listTabs() {
+        return {
+            tabs: [],
+        };
+    }
+
+    listAddons() {
+        return {
+            addons: [],
+        };
+    }
+
+    listProcesses() {
+        return {
+            processes: [this.mainProcessActor.form()],
+        };
+    }
+
+    getRoot() {
+        return {
+            deviceActor: this.deviceActor.actorID,
+            preferenceActor: this.preferenceActor.actorID,
+            addonsActor: null,
+            heapSnapshotFileActor: null,
+            perfActor: null,
+            parentAccessibilityActor: null,
+            screenshotActor: null,
+        };
+    }
+}
+
+class DebuggingConnection {
+    /**
+     * @param {number} connectionId
+     */
+    constructor(connectionId) {
+        this.connectionId = connectionId;
+
+        this.preferenceActor = new PreferenceActor(connectionId);
+        this.mainProcessActor = new ProcessDescriptorActor(connectionId);
+        this.deviceActor = new DeviceActor(connectionId);
+        this.rootActor = new RootActor(
+            connectionId,
+            this.deviceActor,
+            this.preferenceActor,
+            this.mainProcessActor
+        );
+
+        connectionPool.set(connectionId, this);
+    }
+}
+
+class RemoteDebugger {
+    constructor() {}
+
+    /**
+     *
+     * @param {number} to
+     * @param {*} json
+     */
+    writePacket(to, json) {
+        const bytes = JSON.stringify(json);
+        debug(bytes);
+        writeMessage(to, `${bytes.length}:${bytes}`);
+    }
+
+    onReadPacket(connectionId, json) {
+        debug(JSON.stringify(json, null, 4));
+
+        const {to, type, ...options} = json;
+        let actor = actorMap.get(to);
+        let from = actor?.actorID;
+
+        if (!actor && to === 'root') {
+            actor = actorMap.get(`root${connectionId}`);
+            from = 'root';
+        }
+
+        if (!actor) {
+            this.writePacket(connectionId, {
+                from: json.to,
+                error: 'noSuchActor',
+            });
+
+            return;
+        }
+
+        if (type in actor) {
+            this.writePacket(connectionId, {
+                from,
+                ...actor[type]?.(options) ?? null,
+            });
+        } else {
+            this.writePacket(connectionId, {
+                from,
+                // TODO: Find the correct error code for this.
+                error: 'noSuchProperty',
+            });
+        }
+    }
+
+    /**
+     * @param {number} connectionId
+     * @param {string} bytes
+     */
+    onReadMessage(connectionId, bytes) {
+        debug(bytes);
+
+        let parsingBytes = bytes;
+        try {
+            const packets = [];
+            while (parsingBytes.length > 0) {
+                const [length, ...messageParts] = parsingBytes.split(':');
+                const message = messageParts.join(':');
+
+                const parsedLength = Number.parseInt(length, 10);
+                if (Number.isNaN(parsedLength))
+                    throw new Error(`Invalid length: ${length}`);
+
+
+                parsingBytes = message.slice(parsedLength).trim();
+
+                packets.push(JSON.parse(message.slice(0, parsedLength)));
+            }
+
+            packets.forEach(packet => {
+                this.onReadPacket(connectionId, packet);
+            });
+        } catch (error) {
+            debug(error);
+            debug(`Failed to parse: ${bytes}`);
+        }
+    }
+
+    start(port) {
+        startRemoteDebugging(port);
+    }
+
+    sayHello(connection) {
+        this.writePacket(connection, {
+            from: 'root',
+            applicationType: 'gjs',
+            // TODO
+            testConnectionPrefix: connectionPrefix(connection).join('.'),
+            traits: {},
+        });
+    }
+}
+
+var remoteDebugger = new RemoteDebugger();
+
+function onMessage(connectionId, message) {
+    remoteDebugger.onReadMessage(connectionId, message);
+}
+globalThis.onReadMessage = onMessage;
+
+globalThis.onConnection = connectionId => {
+    new DebuggingConnection(connectionId);
+
+    remoteDebugger.sayHello(connectionId);
+};
+
+remoteDebugger.start(6080);
+debug('Starting remote debugging on port 6080...');
diff --git a/translate.js b/translate.js
new file mode 100644
index 00000000..85f6ff43
--- /dev/null
+++ b/translate.js
@@ -0,0 +1,43 @@
+// A simple script to translate Wireshark packet dumps.
+
+const fs = require('fs');
+
+const txt = fs.readFileSync(`./${process.argv[2]}.packets.json`, 'utf-8');
+const json = JSON.parse(txt);
+
+console.log(
+    JSON.stringify(
+        json.map(packet => {
+            const data = packet?._source?.layers?.data?.['data.data'] ?? null;
+
+            if (!data)
+                return null;
+            try {
+                const decoder = new TextDecoder();
+                let parsed = decoder.decode(
+                    new Uint8Array([
+                        ...data.split(':').map(i => Number.parseInt(i, 16)),
+                    ])
+                );
+                let packets = [];
+                while (parsed.length > 0) {
+                    try {
+                        const [length, ...parts] = parsed.split(':');
+                        const remaining = parts.join(':');
+                        const len = Number.parseInt(length);
+                        packets.push(JSON.parse(remaining.slice(0, len)));
+                        parsed = remaining.slice(len);
+                    } catch {
+                        break;
+                    }
+                }
+
+                return packets.length ? packets : packets[0];
+            } catch (err) {
+                return data;
+            }
+        }),
+        null,
+        4
+    )
+);


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