[gjs/ewlsh/remote-debugging] Add remote debugger
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/remote-debugging] Add remote debugger
- Date: Sun, 1 Aug 2021 03:31:08 +0000 (UTC)
commit 84d2a1df286393faf412d43298ecf0c9ca57ab89
Author: Evan Welsh <contact evanwelsh com>
Date: Sat Jul 31 19:46:46 2021 -0700
Add remote debugger
gjs/console.cpp | 6 +
gjs/context.h | 3 +
gjs/debugger.cpp | 41 ++
gjs/global.h | 3 +-
gjs/remote-server.cpp | 171 +++++++
gjs/remote-server.h | 68 +++
gjs/socket-connection.cpp | 222 +++++++++
gjs/socket-connection.h | 91 ++++
gjs/socket-monitor.cpp | 47 ++
gjs/socket-monitor.h | 33 ++
js.gresource.xml | 3 +-
meson.build | 3 +
modules/script/_bootstrap/remoteDebugger.js | 701 ++++++++++++++++++++++++++++
13 files changed, 1390 insertions(+), 2 deletions(-)
---
diff --git a/gjs/console.cpp b/gjs/console.cpp
index 49c82299..adedc4ed 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,8 @@ 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 +384,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..0ac1ac78 100644
--- a/gjs/debugger.cpp
+++ b/gjs/debugger.cpp
@@ -38,6 +38,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 +110,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 +142,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..f8968ee5
--- /dev/null
+++ b/gjs/remote-server.cpp
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017 Igalia S.L.
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include <config.h>
+
+#include <gio/gio.h>
+
+#include <utility>
+
+#include "gjs/remote-server.h"
+#include "gjs/socket-connection.h"
+#include "gjs/socket-monitor.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..3ea4ff75
--- /dev/null
+++ b/gjs/remote-server.h
@@ -0,0 +1,68 @@
+// 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 <string>
+#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 // GJS_REMOTE_SERVER_H_
diff --git a/gjs/socket-connection.cpp b/gjs/socket-connection.cpp
new file mode 100644
index 00000000..24dbb7f8
--- /dev/null
+++ b/gjs/socket-connection.cpp
@@ -0,0 +1,222 @@
+// 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 <string>
+#include <utility>
+#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 constexpr unsigned kDefaultBufferSize = 4096;
+
+SocketConnection::SocketConnection(int32_t id, GSocketConnection* connection,
+ RemoteDebuggingServer* server)
+ : m_id(id), m_server(server), m_connection(connection) {
+ g_object_ref(m_connection);
+
+ m_readBuffer.reserve(kDefaultBufferSize);
+ m_writeBuffer.reserve(kDefaultBufferSize);
+
+ 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() + kDefaultBufferSize);
+
+ GError* error = nullptr;
+ char bytes[kDefaultBufferSize];
+ auto bytesRead =
+ g_socket_receive(g_socket_connection_get_socket(m_connection),
+ bytes, kDefaultBufferSize, 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::string content;
+ content.reserve(m_readBuffer.size());
+ content = std::string(m_readBuffer.begin(), m_readBuffer.end());
+
+ m_readBuffer.erase(m_readBuffer.begin(),
+ m_readBuffer.begin() + content.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(ewlsh): 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..da05388f
--- /dev/null
+++ b/gjs/socket-connection.h
@@ -0,0 +1,91 @@
+// 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 <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_
diff --git a/gjs/socket-monitor.cpp b/gjs/socket-monitor.cpp
new file mode 100644
index 00000000..5fc43b45
--- /dev/null
+++ b/gjs/socket-monitor.cpp
@@ -0,0 +1,47 @@
+// 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"
+
+#include <utility>
+
+GSocketMonitor::~GSocketMonitor() { stop(); }
+
+gboolean 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>(
+ reinterpret_cast<GCallback>(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..60fbf7b3
--- /dev/null
+++ b/gjs/socket-monitor.h
@@ -0,0 +1,33 @@
+// 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 gboolean socketSourceCallback(GSocket*, GIOCondition,
+ GSocketMonitor*);
+
+ GSource* m_source;
+ GCancellable* m_cancellable;
+ std::function<bool(GIOCondition)> m_callback;
+};
+
+#endif // GJS_SOCKET_MONITOR_H_
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..091c7767
--- /dev/null
+++ b/modules/script/_bootstrap/remoteDebugger.js
@@ -0,0 +1,701 @@
+/* global debuggee, loadNative, writeMessage, startRemoteDebugging, */
+// 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} = loadNative('_print');
+/** @type {(msg: string) => void} */
+const debug = print;
+
+function connectionPrefix(connectionId) {
+ return ['gjs', `conn${connectionId}`];
+}
+
+/** @type {Map<string, number>} */
+const actorCount = new Map();
+const connectionPool = new Map();
+
+/**
+ * Makes an identifier unique by appending a counter to it
+ *
+ * @param {string} baseIdentifier the identifier that needs to be made unique
+ * @returns {number}
+ */
+function getActorCount(baseIdentifier) {
+ const count = actorCount.get(baseIdentifier) ?? 1;
+
+ actorCount.set(baseIdentifier, count + 1);
+
+ return count;
+}
+
+/**
+ *
+ * @param {number} connectionId the connection id for this actor
+ * @param {string} actorType the actor type name for this identifier
+ */
+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 an actor to register by ID
+ */
+function registerActor(actor) {
+ actorMap.set(actor.actorID, actor);
+}
+
+class Actor {
+ /**
+ * @param {number} connectionId the connection id for this actor
+ * @param {string} typeName the type name for this actor
+ * @param {string} actorID the unique identifier for this actor
+ */
+ 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 the connection id for this actor
+ * @param {string} typeName the type name for this actor
+ */
+ constructor(connectionId, typeName) {
+ super(
+ connectionId,
+ typeName,
+ createId(connectionId, `${typeName}Actor`)
+ );
+ }
+}
+
+class TargetActor extends Actor {
+ /**
+ * @param {number} connectionId the connection id for this actor
+ * @param {string} typeName the type name for this actor
+ * @param {GlobalActor} parent the parent global actor for this actor
+ */
+ constructor(connectionId, typeName, parent) {
+ super(connectionId, typeName, childOf(parent.actorID, typeName));
+ }
+}
+
+class DeviceActor extends GlobalActor {
+ constructor(connectionId) {
+ super(connectionId, 'device');
+ }
+
+ getDescription() {
+ return {
+ value: {
+ // TODO(ewlsh): Create UUID
+ appid: '{ec8230f7-c20a-464f-9b0e-13a3a9397381}',
+ apptype: 'gjs',
+ vendor: 'GNOME',
+ brandName: 'gjs',
+ name: 'gjs',
+ // TODO(ewlsh): 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 a Source object
+ *
+ * @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 the connection id for this actor
+ * @param {ProcessDescriptorActor} processDescriptorActor the process descriptor actor this source
should be registered to
+ * @param {Debugger.Source} debuggerSource the Source object this actor wraps
+ */
+ 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 the connection id for this actor
+ * @param {ProcessDescriptorActor} processTargetActor the process target this thread's sources should be
registered to
+ */
+ 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(
+ // TODO(ewlsh): Options:
+ // 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()),
+ };
+ }
+
+ // TODO(ewlsh): Handle thread reconfiguring
+ reconfigure() {
+ return {};
+ }
+
+ form() {
+ return {
+ threadActor: {
+ actor: this.actorID,
+ },
+ };
+ }
+}
+
+class ConsoleActor extends TargetActor {
+ /**
+ * @param {number} connectionId the connection id for this actor
+ * @param {ContentProcess} contentProcessActor the content process actor this console is connected to
+ */
+ 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 the connection id for this actor
+ */
+ constructor(connectionId) {
+ super(
+ connectionId,
+ 'content-process',
+ createId(connectionId, 'content-process')
+ );
+
+ this.consoleActor = new ConsoleActor(connectionId, this);
+ }
+}
+
+class ContentProcessTargetActor extends TargetActor {
+ /**
+ *
+ * @param {number} connectionId the connection id for this actor
+ * @param {ProcessDescriptorActor} parentActor the process descriptor this target belongs to
+ */
+ 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 the connection id for this actor
+ * @param {ContentProcess} contentProcessActor the content process this watcher "watches"
+ */
+ constructor(connectionId, contentProcessActor) {
+ super(connectionId, 'watcher', contentProcessActor);
+
+ this.processDescriptorActor = contentProcessActor;
+ }
+
+ form() {
+ return {
+ actor: this.actorID,
+ };
+ }
+}
+
+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 the connection id for this actor
+ */
+ constructor(connectionId) {
+ super(connectionId, 'pref');
+ }
+
+ getBoolPref() {
+ return {
+ value: false,
+ };
+ }
+}
+
+class RootActor extends Actor {
+ /**
+ * @param {number} connectionId the connection id for this actor
+ * @param {DeviceActor} deviceActor the global device actor
+ * @param {PreferenceActor} preferenceActor the global preferences actor
+ * @param {ProcessDescriptorActor} mainProcessActor the global "main" process actor
+ */
+ 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 the connection to wrap and register actors to
+ */
+ 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 {
+ /**
+ *
+ * @param {number} receiverConnectionId the connection to write the packet to
+ * @param {*} json a json packet object to send
+ */
+ writePacket(receiverConnectionId, json) {
+ const packetString = JSON.stringify(json);
+
+ debug(packetString);
+
+ writeMessage(
+ receiverConnectionId,
+ `${packetString.length}:${packetString}`
+ );
+ }
+
+ /**
+ * @param {number} connectionId the connection a packet was received on
+ * @param {*} json the json packet received
+ * @returns {void}
+ */
+ onReadPacket(connectionId, json) {
+ debug(JSON.stringify(json, null, 4));
+
+ const {to, type, ...options} = json;
+ let actor = actorMap.get(to);
+ let from = actor?.actorID;
+
+ // We store root actors as root1, root2, etc.
+ // in our pool to uniquely identify them across
+ // connections. Clients always expect 'root',
+ // so we strip the unique identifier here.
+ 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(ewlsh): Find the correct error code for this.
+ error: 'noSuchProperty',
+ });
+ }
+ }
+
+ /**
+ * @param {number} connectionId the connection the message was read from
+ * @param {string} packetString a string of simple packets
+ */
+ onReadMessage(connectionId, packetString) {
+ debug(packetString);
+
+ let parsingBytes = packetString;
+ 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: ${packetString}`);
+ }
+ }
+
+ start(port) {
+ startRemoteDebugging(port);
+ }
+
+ sayHello(connection) {
+ this.writePacket(connection, {
+ from: 'root',
+ applicationType: 'gjs',
+ // TODO(ewlsh)
+ 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...');
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]