Skip to content

Commit cd72251

Browse files
Add platform support for JS content debugging (#218)
This commit introduces platform support for debugging JavaScript. "Platform support" here means that the PR doesn't contain an implementation of an actual debugger. Instead, it adds infrastructure to StarlingMonkey to initiate a debugging session over a TCP socket connection, and to load a JS implementation of an actual debugger via that connection. The debugger itself works, but isn't quite ready for prime-time, so I'll open it as a draft PR, building on this one. The debugger requires sort of a double opt-in: StarlingMonkey has to be started with the -d/--enable-script-debugging option set, and a port to connect to needs to be provided via the DEBUGGER_PORT env var. That ensures that StarlingMonkey will not try to read env vars at runtime without a prior opt-in, and hence will guarantee that it'll continue working in environments that stub out wasi::environment.
1 parent 1038c49 commit cd72251

File tree

14 files changed

+581
-17
lines changed

14 files changed

+581
-17
lines changed

CMakeLists.txt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ include("openssl")
3838
include("${HOST_API}/host_api.cmake")
3939
include("build-crates")
4040

41+
option(ENABLE_JS_DEBUGGER "Enable support for debugging content via a socket connection" ON)
42+
4143
add_library(extension_api INTERFACE include/extension-api.h runtime/encode.h runtime/decode.h)
44+
if (ENABLE_JS_DEBUGGER)
45+
target_compile_definitions(extension_api INTERFACE ENABLE_JS_DEBUGGER)
46+
endif()
4247
target_link_libraries(extension_api INTERFACE rust-url spidermonkey)
4348
target_include_directories(extension_api INTERFACE include deps/include runtime)
4449

@@ -58,8 +63,11 @@ add_executable(starling-raw.wasm
5863
runtime/event_loop.cpp
5964
runtime/builtin.cpp
6065
runtime/script_loader.cpp
66+
runtime/debugger.cpp
6167
)
6268

69+
target_link_libraries(starling-raw.wasm PRIVATE host_api extension_api builtins spidermonkey rust-url multipart)
70+
6371
option(USE_WASM_OPT "use wasm-opt to optimize the StarlingMonkey binary" ON)
6472

6573
# For release builds, use wasm-opt to optimize the generated wasm file.
@@ -90,8 +98,6 @@ else()
9098
endif()
9199
endif()
92100

93-
target_link_libraries(starling-raw.wasm PRIVATE host_api extension_api builtins spidermonkey rust-url multipart)
94-
95101
# build a compilation cache of ICs
96102
if(WEVAL)
97103
include("weval")

builtins/web/fetch/fetch_event.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "../dom-exception.h"
99

1010
#include <allocator.h>
11+
#include <debugger.h>
1112
#include <js/SourceText.h>
1213

1314
#include <iostream>
@@ -543,6 +544,7 @@ bool handle_incoming_request(host_api::HttpIncomingRequest *request) {
543544

544545
double total_compute = 0;
545546

547+
content_debugger::maybe_init_debugger(ENGINE, true);
546548
dispatch_fetch_event(fetch_event, &total_compute);
547549

548550
bool success = ENGINE->run_event_loop();

cmake/CPM.cmake

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
#
33
# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors
44

5-
set(CPM_DOWNLOAD_VERSION 0.38.7)
6-
set(CPM_HASH_SUM "83e5eb71b2bbb8b1f2ad38f1950287a057624e385c238f6087f94cdfc44af9c5")
5+
set(CPM_DOWNLOAD_VERSION 0.40.5)
6+
set(CPM_HASH_SUM "c46b876ae3b9f994b4f05a4c15553e0485636862064f1fcc9d8b4f832086bc5d")
77

88
# Ensure that the CPM_SOURCE_CACHE is defined and in sync with ENV{CPM_SOURCE_CACHE}
99
if (NOT DEFINED CPM_SOURCE_CACHE)

host-apis/wasi-0.2.0/host_api.cmake

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ add_library(host_api STATIC
33
${HOST_API}/host_call.cpp
44
${HOST_API}/bindings/bindings.c
55
${HOST_API}/bindings/bindings_component_type.o
6-
${CMAKE_CURRENT_SOURCE_DIR}/include/host_api.h
76
)
87

98
target_link_libraries(host_api PRIVATE spidermonkey)

host-apis/wasi-0.2.0/host_api.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include "bindings/bindings.h"
33
#include "handles.h"
44

5+
#include <wasi/libc-environ.h>
6+
57
static std::optional<wasi_clocks_monotonic_clock_own_pollable_t> immediately_ready;
68

79
size_t poll_handles(vector<WASIHandle<host_api::Pollable>::Borrowed> handles) {
@@ -1030,6 +1032,9 @@ void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request reque
10301032
// that it properly initializes the runtime and installs a request handler.
10311033
if (!REQUEST_HANDLER) {
10321034
init_from_environment();
1035+
} else {
1036+
// Resuming a wizer snapshot, so we have to ensure that the environment is reset.
1037+
__wasilibc_initialize_environ();
10331038
}
10341039
MOZ_ASSERT(REQUEST_HANDLER);
10351040

host-apis/wasi-0.2.3/host_api.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ set(WASI_0_2_0 "${HOST_API}/../wasi-0.2.0")
33
add_library(host_api STATIC
44
${WASI_0_2_0}/host_api.cpp
55
${WASI_0_2_0}/host_call.cpp
6+
${HOST_API}/sockets.cpp
67
${HOST_API}/bindings/bindings.c
78
${HOST_API}/bindings/bindings_component_type.o
89
)
@@ -11,6 +12,7 @@ target_link_libraries(host_api PRIVATE spidermonkey)
1112
target_include_directories(host_api PRIVATE include)
1213
target_include_directories(host_api PRIVATE ${WASI_0_2_0})
1314
target_include_directories(host_api PUBLIC ${WASI_0_2_0}/include)
15+
target_include_directories(host_api PUBLIC ${HOST_API}/include)
1416

1517
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
1618
set(ADAPTER "debug")
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#ifndef SOCKETS_H
2+
#define SOCKETS_H
3+
4+
#include "host_api.h"
5+
6+
namespace host_api {
7+
8+
9+
/**
10+
* \class TCPSocket
11+
* \brief A bare-bones representation of a TCP socket, supporting only basic, blocking operations.
12+
*
13+
* This class provides methods to create, connect, send, and receive data over a TCP socket.
14+
*/
15+
class TCPSocket : public Resource {
16+
protected:
17+
TCPSocket(std::unique_ptr<HandleState> state);
18+
19+
public:
20+
using AddressIPV4 = std::tuple<uint8_t, uint8_t, uint8_t, uint8_t>;
21+
using Port = uint16_t;
22+
23+
enum IPAddressFamily {
24+
IPV4,
25+
IPV6,
26+
};
27+
28+
TCPSocket() = delete;
29+
30+
/**
31+
* \brief Factory method to create a TCPSocket.
32+
* \param address_family The IP address family (IPv4 or IPv6).
33+
* \return A pointer to the created TCPSocket.
34+
*/
35+
static TCPSocket* make(IPAddressFamily address_family);
36+
37+
~TCPSocket() override = default;
38+
39+
/**
40+
* \brief Connects the socket to a specified address and port synchronously,
41+
* blocking until the connection is established.
42+
* \param address The IPv4 address to connect to.
43+
* \param port The port to connect to.
44+
* \return True if the connection is successful, false otherwise.
45+
*/
46+
bool connect(AddressIPV4 address, Port port);
47+
48+
/**
49+
* \brief Closes the socket if it is open, no-op otherwise.
50+
*/
51+
void close();
52+
53+
/**
54+
* \brief Sends data over the socket synchronously, blocking until the data is sent.
55+
* \param chunk The data to send.
56+
* \return True if the data is sent successfully, false otherwise.
57+
*/
58+
bool send(HostString chunk);
59+
60+
/**
61+
* \brief Receives data from the socket.
62+
* \param chunk_size The size of the data chunk to receive.
63+
* \return The received data as a HostString.
64+
*/
65+
HostString receive(uint32_t chunk_size);
66+
};
67+
68+
}
69+
70+
#endif //SOCKETS_H

host-apis/wasi-0.2.3/sockets.cpp

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#include "sockets.h"
2+
3+
#include <../wasi-0.2.0/handles.h>
4+
5+
template <> struct HandleOps<host_api::TCPSocket> {
6+
using owned = wasi_sockets_tcp_own_tcp_socket_t;
7+
using borrowed = wasi_sockets_tcp_borrow_tcp_socket_t;
8+
};
9+
10+
class TCPSocketHandle final : public WASIHandle<host_api::TCPSocket> {
11+
wasi_sockets_instance_network_own_network_t network_;
12+
PollableHandle pollable_handle_;
13+
wasi_io_streams_own_input_stream_t input_;
14+
wasi_io_streams_own_output_stream_t output_;
15+
16+
friend host_api::TCPSocket;
17+
18+
public:
19+
explicit TCPSocketHandle(HandleOps<host_api::TCPSocket>::owned handle)
20+
: WASIHandle(handle), pollable_handle_(INVALID_POLLABLE_HANDLE) {
21+
network_ = wasi_sockets_instance_network_instance_network();
22+
}
23+
24+
static TCPSocketHandle *cast(HandleState *handle) {
25+
return reinterpret_cast<TCPSocketHandle *>(handle);
26+
}
27+
28+
wasi_sockets_tcp_borrow_network_t network() const {
29+
return wasi_sockets_tcp_borrow_network_t(network_.__handle);
30+
}
31+
PollableHandle pollable_handle() {
32+
if (pollable_handle_ == INVALID_POLLABLE_HANDLE) {
33+
pollable_handle_ = wasi_sockets_tcp_method_tcp_socket_subscribe(borrow()).__handle;
34+
}
35+
return pollable_handle_;
36+
}
37+
};
38+
39+
namespace host_api {
40+
41+
TCPSocket::TCPSocket(std::unique_ptr<HandleState> state) {
42+
this->handle_state_ = std::move(state);
43+
}
44+
45+
TCPSocket *TCPSocket::make(IPAddressFamily address_family) {
46+
wasi_sockets_tcp_create_socket_ip_address_family_t family =
47+
address_family == IPV4 ? WASI_SOCKETS_NETWORK_IP_ADDRESS_FAMILY_IPV4
48+
: WASI_SOCKETS_NETWORK_IP_ADDRESS_FAMILY_IPV6;
49+
wasi_sockets_tcp_create_socket_own_tcp_socket_t ret;
50+
wasi_sockets_tcp_create_socket_error_code_t err;
51+
if (!wasi_sockets_tcp_create_socket_create_tcp_socket(family, &ret, &err)) {
52+
return nullptr;
53+
}
54+
return new TCPSocket(std::unique_ptr<HandleState>(new TCPSocketHandle(ret)));
55+
}
56+
57+
bool TCPSocket::connect(AddressIPV4 address, Port port) {
58+
auto state = TCPSocketHandle::cast(handle_state_.get());
59+
auto handle = state->borrow();
60+
auto addr = wasi_sockets_network_ipv4_address_t(get<0>(address), get<1>(address), get<2>(address),
61+
get<3>(address));
62+
wasi_sockets_tcp_ip_socket_address_t socket_address = {
63+
WASI_SOCKETS_NETWORK_IP_SOCKET_ADDRESS_IPV4, {{port, addr}}};
64+
wasi_sockets_tcp_error_code_t err;
65+
if (!wasi_sockets_tcp_method_tcp_socket_start_connect(handle, state->network(), &socket_address,
66+
&err)) {
67+
// TODO: handle error
68+
return false;
69+
}
70+
71+
wasi_sockets_tcp_tuple2_own_input_stream_own_output_stream_t streams;
72+
while (true) {
73+
if (!wasi_sockets_tcp_method_tcp_socket_finish_connect(handle, &streams, &err)) {
74+
if (err == WASI_SOCKETS_NETWORK_ERROR_CODE_WOULD_BLOCK) {
75+
block_on_pollable_handle(state->pollable_handle());
76+
continue;
77+
}
78+
// TODO: handle error
79+
return false;
80+
}
81+
state->input_ = streams.f0;
82+
state->output_ = streams.f1;
83+
break;
84+
}
85+
86+
return true;
87+
}
88+
void TCPSocket::close() {
89+
auto state = TCPSocketHandle::cast(handle_state_.get());
90+
if (!state->valid()) {
91+
return;
92+
}
93+
wasi_sockets_tcp_error_code_t err;
94+
wasi_sockets_tcp_method_tcp_socket_shutdown(state->borrow(),
95+
WASI_SOCKETS_TCP_SHUTDOWN_TYPE_BOTH, &err);
96+
wasi_io_streams_output_stream_drop_own(state->output_);
97+
wasi_io_streams_input_stream_drop_own(state->input_);
98+
if (state->pollable_handle_ != INVALID_POLLABLE_HANDLE) {
99+
wasi_io_poll_pollable_drop_own(own_pollable_t{state->pollable_handle_});
100+
}
101+
wasi_sockets_tcp_tcp_socket_drop_own(state->take());
102+
}
103+
104+
bool TCPSocket::send(HostString chunk) {
105+
auto state = TCPSocketHandle::cast(handle_state_.get());
106+
auto borrow = wasi_io_streams_borrow_output_stream(state->output_);
107+
bindings_list_u8_t list{reinterpret_cast<uint8_t *>(chunk.ptr.get()), chunk.len};
108+
uint64_t capacity = 0;
109+
wasi_io_streams_stream_error_t err;
110+
if (!wasi_io_streams_method_output_stream_check_write(borrow, &capacity, &err)) {
111+
// TODO: proper error handling.
112+
}
113+
// TODO: proper error handling.
114+
MOZ_ASSERT(chunk.len <= capacity);
115+
return wasi_io_streams_method_output_stream_write(borrow, &list, &err);
116+
}
117+
118+
HostString TCPSocket::receive(uint32_t chunk_size) {
119+
auto state = TCPSocketHandle::cast(handle_state_.get());
120+
auto borrow = wasi_io_streams_borrow_input_stream(state->input_);
121+
bindings_list_u8_t ret{};
122+
wasi_io_streams_stream_error_t err{};
123+
mozilla::DebugOnly<bool> success;
124+
success = wasi_io_streams_method_input_stream_blocking_read(borrow, chunk_size, &ret, &err);
125+
MOZ_ASSERT(success, "Why you not handle errors");
126+
UniqueChars chars((char*)ret.ptr);
127+
return HostString(std::move(chars), ret.len);
128+
}
129+
130+
} // namespace host_api

include/config-parser.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ConfigParser {
1313

1414
public:
1515
ConfigParser() : config_(std::make_unique<api::EngineConfig>()) {
16-
config_->content_script_path = DEFAULT_SCRIPT_PATH;
16+
config_->content_script_path = mozilla::Some(DEFAULT_SCRIPT_PATH);
1717
}
1818

1919
/**
@@ -73,23 +73,25 @@ class ConfigParser {
7373
for (size_t i = 1; i < args.size(); i++) {
7474
if (args[i] == "-e" || args[i] == "--eval") {
7575
if (i + 1 < args.size()) {
76-
config_->content_script = args[i + 1];
76+
config_->content_script = mozilla::Some(args[i + 1]);
7777
config_->content_script_path.reset();
7878
i++;
7979
}
8080
} else if (args[i] == "-v" || args[i] == "--verbose") {
8181
config_->verbose = true;
82+
} else if (args[i] == "-d" || args[i] == "--enable-script-debugging") {
83+
config_->debugging = true;
8284
} else if (args[i] == "--legacy-script") {
8385
config_->module_mode = false;
8486
if (i + 1 < args.size()) {
85-
config_->content_script_path = args[i + 1];
87+
config_->content_script_path = mozilla::Some(args[i + 1]);
8688
i++;
8789
}
8890
} else if (args[i].starts_with("--")) {
8991
std::cerr << "Unknown option: " << args[i] << std::endl;
9092
exit(1);
9193
} else {
92-
config_->content_script_path = args[i];
94+
config_->content_script_path = mozilla::Some(args[i]);
9395
}
9496
}
9597

include/extension-api.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ namespace api {
3535
class AsyncTask;
3636

3737
struct EngineConfig {
38-
optional<std::string> content_script_path;
39-
optional<std::string> content_script;
38+
mozilla::Maybe<std::string> content_script_path;
39+
mozilla::Maybe<std::string> content_script;
4040
bool module_mode = true;
4141

4242
/**
@@ -47,6 +47,14 @@ struct EngineConfig {
4747
bool pre_initialize = false;
4848
bool verbose = false;
4949

50+
/**
51+
* Whether to enable the script debugger. If this is enabled, the runtime will
52+
* check for the DEBUGGER_PORT environment variable and try to connect to that
53+
* port on localhost if it's set. If that succeeds, it expects the host to send
54+
* a script to use as the debugger, using the SpiderMonkey Debugger API.
55+
*/
56+
bool debugging = false;
57+
5058
EngineConfig() = default;
5159
};
5260

@@ -63,6 +71,7 @@ class Engine {
6371
JSContext *cx();
6472
HandleObject global();
6573
EngineState state();
74+
bool debugging_enabled();
6675

6776
void finish_pre_initialization();
6877

0 commit comments

Comments
 (0)