@react-native-quickjs/quickjs 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/NOTICE +31 -0
- package/README.md +217 -0
- package/ReactNativeQuickJS.podspec +197 -0
- package/android/CMakeLists.txt +134 -0
- package/android/build.gradle +214 -0
- package/android/quickjs.gradle +39 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSInstance.kt +39 -0
- package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSPackage.kt +32 -0
- package/android/src/main/jni/JJSRuntimeFactory.h +37 -0
- package/android/src/main/jni/JQuickJSInstance.cpp +27 -0
- package/android/src/main/jni/JQuickJSInstance.h +46 -0
- package/android/src/main/jni/OnLoad.cpp +15 -0
- package/app.plugin.js +11 -0
- package/apple/RCTQuickJSInstanceFactory.h +32 -0
- package/apple/RCTQuickJSInstanceFactory.mm +16 -0
- package/bin/qjsc/darwin-arm64/qjsc +0 -0
- package/bin/qjsc/darwin-x64/qjsc +0 -0
- package/bin/qjsc/linux-arm64/qjsc +0 -0
- package/bin/qjsc/linux-x64/qjsc +0 -0
- package/bin/qjsc/manifest.json +11 -0
- package/bin/qjsc/win32-x64/qjsc.exe +0 -0
- package/bin/react-native-quickjs.js +28 -0
- package/cmake/jsi.cmake +24 -0
- package/cmake/quickjs.cmake +125 -0
- package/engine/quickjs-rel/LICENSE +24 -0
- package/engine/quickjs-rel/MANIFEST.json +55 -0
- package/engine/quickjs-rel/builtin-array-fromasync.h +120 -0
- package/engine/quickjs-rel/builtin-iterator-zip-keyed.h +332 -0
- package/engine/quickjs-rel/builtin-iterator-zip.h +337 -0
- package/engine/quickjs-rel/cutils.h +1998 -0
- package/engine/quickjs-rel/dtoa.c +1619 -0
- package/engine/quickjs-rel/dtoa.h +87 -0
- package/engine/quickjs-rel/libregexp-opcode.h +73 -0
- package/engine/quickjs-rel/libregexp.c +3478 -0
- package/engine/quickjs-rel/libregexp.h +101 -0
- package/engine/quickjs-rel/libunicode-table.h +5173 -0
- package/engine/quickjs-rel/libunicode.c +2069 -0
- package/engine/quickjs-rel/libunicode.h +172 -0
- package/engine/quickjs-rel/list.h +107 -0
- package/engine/quickjs-rel/quickjs-atom.h +280 -0
- package/engine/quickjs-rel/quickjs-c-atomics.h +54 -0
- package/engine/quickjs-rel/quickjs-opcode.h +385 -0
- package/engine/quickjs-rel/quickjs.c +64854 -0
- package/engine/quickjs-rel/quickjs.h +1577 -0
- package/modules/cdp/quickjs-cdp.c +1386 -0
- package/modules/cdp/quickjs-cdp.h +122 -0
- package/modules/cdp/react/QuickJSInspector.cpp +322 -0
- package/modules/cdp/react/QuickJSInspector.h +114 -0
- package/modules/hermes-compat/include/hermes/DebuggerAPI.h +20 -0
- package/modules/hermes-compat/include/hermes/Public/CrashManager.h +47 -0
- package/modules/hermes-compat/include/hermes/Public/CtorConfig.h +88 -0
- package/modules/hermes-compat/include/hermes/Public/GCConfig.h +83 -0
- package/modules/hermes-compat/include/hermes/Public/HermesExport.h +17 -0
- package/modules/hermes-compat/include/hermes/Public/RuntimeConfig.h +66 -0
- package/modules/hermes-compat/include/hermes/Public/SamplingProfiler.h +22 -0
- package/modules/hermes-compat/include/hermes/hermes.h +195 -0
- package/modules/hermes-compat/include/hermes/inspector/RuntimeAdapter.h +43 -0
- package/modules/hermes-compat/include/hermes/inspector-modern/chrome/Registration.h +31 -0
- package/modules/hermes-compat/include/hermes-compat/Diagnostics.h +35 -0
- package/modules/hermes-compat/src/HermesCompat.cpp +557 -0
- package/package.json +104 -0
- package/scripts/bytecode/compile.js +99 -0
- package/scripts/expo/plugin.js +231 -0
- package/scripts/postinstall.js +74 -0
- package/scripts/react_native_quickjs_pods.rb +237 -0
- package/scripts/setup/edits.js +311 -0
- package/scripts/setup/run.js +121 -0
- package/src/bytecode/QuickJSBytecode.cpp +50 -0
- package/src/bytecode/QuickJSBytecode.h +50 -0
- package/src/module/QuickJSCompat.cpp +92 -0
- package/src/module/QuickJSCompat.h +19 -0
- package/src/module/QuickJSModule.cpp +70 -0
- package/src/module/QuickJSModule.h +108 -0
- package/src/module/QuickJSModuleNative.h +93 -0
- package/src/runtime/QuickJSInstance.cpp +37 -0
- package/src/runtime/QuickJSInstance.h +74 -0
- package/src/runtime/QuickJSRuntime.cpp +1864 -0
- package/src/runtime/QuickJSRuntime.h +529 -0
- package/src/runtime/QuickJSRuntimeConfig.h +103 -0
- package/src/runtime/QuickJSRuntimeFactory.cpp +34 -0
- package/src/runtime/QuickJSRuntimeFactory.h +24 -0
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Ammar Ahmed.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* The Chrome DevTools Protocol, for QuickJS. See quickjs-cdp.h.
|
|
8
|
+
*
|
|
9
|
+
* Protocol messages are JSON and the engine already parses JSON, so there is
|
|
10
|
+
* no JSON library here and no generated marshalling code: a message is a
|
|
11
|
+
* JSValue on a private context, and a reply is built by setting properties on
|
|
12
|
+
* another one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
#include "quickjs-cdp.h"
|
|
16
|
+
|
|
17
|
+
#include <math.h>
|
|
18
|
+
#include <pthread.h>
|
|
19
|
+
#include <stdbool.h>
|
|
20
|
+
#include <stdint.h>
|
|
21
|
+
#include <stdio.h>
|
|
22
|
+
#include <stdlib.h>
|
|
23
|
+
#include <string.h>
|
|
24
|
+
|
|
25
|
+
typedef struct {
|
|
26
|
+
int id;
|
|
27
|
+
char *url;
|
|
28
|
+
char *source;
|
|
29
|
+
} Script;
|
|
30
|
+
|
|
31
|
+
typedef struct {
|
|
32
|
+
int id;
|
|
33
|
+
char *url;
|
|
34
|
+
char *condition;
|
|
35
|
+
int line;
|
|
36
|
+
int col;
|
|
37
|
+
bool resolved;
|
|
38
|
+
} Breakpoint;
|
|
39
|
+
|
|
40
|
+
typedef struct {
|
|
41
|
+
int id;
|
|
42
|
+
JSValue value;
|
|
43
|
+
char *group;
|
|
44
|
+
} RemoteObject;
|
|
45
|
+
|
|
46
|
+
typedef enum {
|
|
47
|
+
MODE_RUN,
|
|
48
|
+
MODE_PAUSE_NEXT,
|
|
49
|
+
MODE_STEP_OVER,
|
|
50
|
+
MODE_STEP_INTO,
|
|
51
|
+
MODE_STEP_OUT
|
|
52
|
+
} PauseMode;
|
|
53
|
+
|
|
54
|
+
struct QJSCDPAgent {
|
|
55
|
+
JSContext *ctx;
|
|
56
|
+
|
|
57
|
+
/* Protocol JSON is parsed and printed on a runtime of the agent's own, so
|
|
58
|
+
that handling a message cannot allocate in, collect from or throw into the
|
|
59
|
+
heap the debugger exists to observe. */
|
|
60
|
+
JSRuntime *jrt;
|
|
61
|
+
JSContext *jctx;
|
|
62
|
+
|
|
63
|
+
int exec_ctx_id;
|
|
64
|
+
QJSCDPSendFunc *send;
|
|
65
|
+
void *send_opaque;
|
|
66
|
+
|
|
67
|
+
/* Messages arrive on the inspector thread and are handled on the JS thread.
|
|
68
|
+
The condition variable is what lets the pause loop sleep rather than spin. */
|
|
69
|
+
pthread_mutex_t lock;
|
|
70
|
+
pthread_cond_t cond;
|
|
71
|
+
char **queue;
|
|
72
|
+
void **queue_session;
|
|
73
|
+
int queue_len, queue_cap;
|
|
74
|
+
|
|
75
|
+
/* The session whose request is being handled right now. Replies go to it;
|
|
76
|
+
events go to everyone. */
|
|
77
|
+
void *session;
|
|
78
|
+
|
|
79
|
+
Script *scripts;
|
|
80
|
+
int nscripts;
|
|
81
|
+
Breakpoint *bps;
|
|
82
|
+
int nbps, next_bp_id;
|
|
83
|
+
RemoteObject *objs;
|
|
84
|
+
int nobjs, next_obj_id;
|
|
85
|
+
|
|
86
|
+
bool debugger_enabled;
|
|
87
|
+
bool breakpoints_active;
|
|
88
|
+
|
|
89
|
+
PauseMode mode;
|
|
90
|
+
int step_depth;
|
|
91
|
+
bool paused;
|
|
92
|
+
bool resumed;
|
|
93
|
+
bool pause_pending;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/* ---------------------------------------------------------------- JSON --- */
|
|
97
|
+
|
|
98
|
+
static JSValue json_object(QJSCDPAgent *agent) {
|
|
99
|
+
return JS_NewObject(agent->jctx);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static JSValue json_array(QJSCDPAgent *agent) {
|
|
103
|
+
return JS_NewArray(agent->jctx);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static void json_set(QJSCDPAgent *agent, JSValue o, const char *k, JSValue v) {
|
|
107
|
+
JS_SetPropertyStr(agent->jctx, o, k, v);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static void json_set_string(
|
|
111
|
+
QJSCDPAgent *agent, JSValue o, const char *k, const char *v) {
|
|
112
|
+
json_set(agent, o, k, JS_NewString(agent->jctx, v ? v : ""));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static void json_set_int(
|
|
116
|
+
QJSCDPAgent *agent, JSValue o, const char *k, int64_t v) {
|
|
117
|
+
json_set(agent, o, k, JS_NewInt64(agent->jctx, v));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static void json_set_bool(
|
|
121
|
+
QJSCDPAgent *agent, JSValue o, const char *k, bool v) {
|
|
122
|
+
json_set(agent, o, k, JS_NewBool(agent->jctx, v));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static void json_set_index(
|
|
126
|
+
QJSCDPAgent *agent, JSValue arr, uint32_t i, JSValue v) {
|
|
127
|
+
JS_SetPropertyUint32(agent->jctx, arr, i, v);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static JSValue json_get(QJSCDPAgent *agent, JSValue o, const char *k) {
|
|
131
|
+
return JS_GetPropertyStr(agent->jctx, o, k);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Result must be released with JS_FreeCString(agent->jctx, ...). NULL when the
|
|
135
|
+
property is absent or null. */
|
|
136
|
+
static const char *json_get_string(
|
|
137
|
+
QJSCDPAgent *agent, JSValue o, const char *k) {
|
|
138
|
+
JSValue v = json_get(agent, o, k);
|
|
139
|
+
if (JS_IsUndefined(v) || JS_IsNull(v) || JS_IsException(v)) {
|
|
140
|
+
JS_FreeValue(agent->jctx, v);
|
|
141
|
+
return NULL;
|
|
142
|
+
}
|
|
143
|
+
const char *s = JS_ToCString(agent->jctx, v);
|
|
144
|
+
JS_FreeValue(agent->jctx, v);
|
|
145
|
+
return s;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static int json_get_int(
|
|
149
|
+
QJSCDPAgent *agent, JSValue o, const char *k, int fallback) {
|
|
150
|
+
JSValue v = json_get(agent, o, k);
|
|
151
|
+
int32_t n = fallback;
|
|
152
|
+
if (JS_IsNumber(v)) JS_ToInt32(agent->jctx, &n, v);
|
|
153
|
+
JS_FreeValue(agent->jctx, v);
|
|
154
|
+
return n;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static bool json_get_bool(
|
|
158
|
+
QJSCDPAgent *agent, JSValue o, const char *k, bool fallback) {
|
|
159
|
+
JSValue v = json_get(agent, o, k);
|
|
160
|
+
bool b = JS_IsBool(v) ? JS_ToBool(agent->jctx, v) : fallback;
|
|
161
|
+
JS_FreeValue(agent->jctx, v);
|
|
162
|
+
return b;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Takes ownership of msg. */
|
|
166
|
+
static void send_message(QJSCDPAgent *agent, JSValue msg) {
|
|
167
|
+
JSValue s = JS_JSONStringify(agent->jctx, msg, JS_UNDEFINED, JS_UNDEFINED);
|
|
168
|
+
size_t len = 0;
|
|
169
|
+
const char *text = JS_ToCStringLen(agent->jctx, &len, s);
|
|
170
|
+
if (text) {
|
|
171
|
+
agent->send(agent->send_opaque, agent->session, text, len);
|
|
172
|
+
JS_FreeCString(agent->jctx, text);
|
|
173
|
+
}
|
|
174
|
+
JS_FreeValue(agent->jctx, s);
|
|
175
|
+
JS_FreeValue(agent->jctx, msg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Takes ownership of result. */
|
|
179
|
+
static void send_result(QJSCDPAgent *agent, int id, JSValue result) {
|
|
180
|
+
JSValue m = json_object(agent);
|
|
181
|
+
json_set_int(agent, m, "id", id);
|
|
182
|
+
json_set(agent, m, "result", result);
|
|
183
|
+
send_message(agent, m);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static void send_empty_result(QJSCDPAgent *agent, int id) {
|
|
187
|
+
send_result(agent, id, json_object(agent));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static void send_error_code(
|
|
191
|
+
QJSCDPAgent *agent, int id, int code, const char *message) {
|
|
192
|
+
JSValue e = json_object(agent);
|
|
193
|
+
json_set_int(agent, e, "code", code);
|
|
194
|
+
json_set_string(agent, e, "message", message);
|
|
195
|
+
JSValue m = json_object(agent);
|
|
196
|
+
json_set_int(agent, m, "id", id);
|
|
197
|
+
json_set(agent, m, "error", e);
|
|
198
|
+
send_message(agent, m);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* -32000 is "server error", which is what the protocol uses for anything the
|
|
202
|
+
engine simply could not do. */
|
|
203
|
+
static void send_error(QJSCDPAgent *agent, int id, const char *message) {
|
|
204
|
+
send_error_code(agent, id, -32000, message);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Takes ownership of params. `everyone` distinguishes something that happened
|
|
208
|
+
in the program, which every attached frontend wants, from a reply-shaped
|
|
209
|
+
event such as the scripts replayed when one frontend enables the debugger --
|
|
210
|
+
which the others have already been told about. */
|
|
211
|
+
static void send_event_to(
|
|
212
|
+
QJSCDPAgent *agent, const char *method, JSValue params, bool everyone) {
|
|
213
|
+
void *asked = agent->session;
|
|
214
|
+
if (everyone) agent->session = NULL;
|
|
215
|
+
JSValue m = json_object(agent);
|
|
216
|
+
json_set_string(agent, m, "method", method);
|
|
217
|
+
json_set(agent, m, "params", params);
|
|
218
|
+
send_message(agent, m);
|
|
219
|
+
agent->session = asked;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
static void send_event(QJSCDPAgent *agent, const char *method, JSValue params) {
|
|
223
|
+
send_event_to(agent, method, params, true);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ------------------------------------------------------- remote objects --- */
|
|
227
|
+
|
|
228
|
+
static int remember_object(QJSCDPAgent *agent, JSValue v, const char *group) {
|
|
229
|
+
agent->objs = realloc(agent->objs, sizeof(RemoteObject) * (agent->nobjs + 1));
|
|
230
|
+
RemoteObject *r = &agent->objs[agent->nobjs++];
|
|
231
|
+
r->id = ++agent->next_obj_id;
|
|
232
|
+
r->value = JS_DupValue(agent->ctx, v);
|
|
233
|
+
r->group = group ? strdup(group) : NULL;
|
|
234
|
+
return r->id;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static RemoteObject *find_object(QJSCDPAgent *agent, const char *object_id) {
|
|
238
|
+
if (!object_id) return NULL;
|
|
239
|
+
int id = atoi(object_id);
|
|
240
|
+
for (int i = 0; i < agent->nobjs; i++)
|
|
241
|
+
if (agent->objs[i].id == id) return &agent->objs[i];
|
|
242
|
+
return NULL;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static void forget_object(QJSCDPAgent *agent, int i) {
|
|
246
|
+
JS_FreeValue(agent->ctx, agent->objs[i].value);
|
|
247
|
+
free(agent->objs[i].group);
|
|
248
|
+
agent->objs[i] = agent->objs[--agent->nobjs];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static bool release_object(QJSCDPAgent *agent, const char *object_id) {
|
|
252
|
+
RemoteObject *r = find_object(agent, object_id);
|
|
253
|
+
if (!r) return false;
|
|
254
|
+
forget_object(agent, (int)(r - agent->objs));
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static void release_object_group(QJSCDPAgent *agent, const char *group) {
|
|
259
|
+
if (!group) return;
|
|
260
|
+
for (int i = agent->nobjs - 1; i >= 0; i--)
|
|
261
|
+
if (agent->objs[i].group && strcmp(agent->objs[i].group, group) == 0)
|
|
262
|
+
forget_object(agent, i);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Debuggee -> C string, swallowing any exception a toString() raises. The
|
|
266
|
+
frontend asked for a description, not for the program's error state. */
|
|
267
|
+
static const char *debuggee_to_string(QJSCDPAgent *agent, JSValue v) {
|
|
268
|
+
const char *s = JS_ToCString(agent->ctx, v);
|
|
269
|
+
if (!s && JS_HasException(agent->ctx))
|
|
270
|
+
JS_FreeValue(agent->ctx, JS_GetException(agent->ctx));
|
|
271
|
+
return s;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
static void free_debuggee_string(QJSCDPAgent *agent, const char *s) {
|
|
275
|
+
JS_FreeCString(agent->ctx, s);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static const char *subtype_name(QJSCDPAgent *agent, JSValue v) {
|
|
279
|
+
if (JS_IsNull(v)) return "null";
|
|
280
|
+
if (JS_IsArray(v)) return "array";
|
|
281
|
+
if (JS_IsError(v)) return "error";
|
|
282
|
+
if (JS_IsDate(v)) return "date";
|
|
283
|
+
if (JS_IsRegExp(v)) return "regexp";
|
|
284
|
+
if (JS_IsMap(v)) return "map";
|
|
285
|
+
if (JS_IsPromise(v)) return "promise";
|
|
286
|
+
if (JS_IsProxy(v)) return "proxy";
|
|
287
|
+
return NULL;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
static void set_description(QJSCDPAgent *agent, JSValue out, JSValue v) {
|
|
291
|
+
const char *s = debuggee_to_string(agent, v);
|
|
292
|
+
if (s) {
|
|
293
|
+
json_set_string(agent, out, "description", s);
|
|
294
|
+
free_debuggee_string(agent, s);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Takes a borrowed debuggee value; returns a protocol-context object. */
|
|
299
|
+
static JSValue to_remote_object(
|
|
300
|
+
QJSCDPAgent *agent, JSValue v, const char *group) {
|
|
301
|
+
JSValue o = json_object(agent);
|
|
302
|
+
|
|
303
|
+
if (JS_IsUndefined(v)) {
|
|
304
|
+
json_set_string(agent, o, "type", "undefined");
|
|
305
|
+
return o;
|
|
306
|
+
}
|
|
307
|
+
if (JS_IsNull(v)) {
|
|
308
|
+
json_set_string(agent, o, "type", "object");
|
|
309
|
+
json_set_string(agent, o, "subtype", "null");
|
|
310
|
+
json_set(agent, o, "value", JS_NULL);
|
|
311
|
+
return o;
|
|
312
|
+
}
|
|
313
|
+
if (JS_IsBool(v)) {
|
|
314
|
+
json_set_string(agent, o, "type", "boolean");
|
|
315
|
+
json_set_bool(agent, o, "value", JS_ToBool(agent->ctx, v));
|
|
316
|
+
return o;
|
|
317
|
+
}
|
|
318
|
+
if (JS_IsNumber(v)) {
|
|
319
|
+
double d;
|
|
320
|
+
JS_ToFloat64(agent->ctx, &d, v);
|
|
321
|
+
json_set_string(agent, o, "type", "number");
|
|
322
|
+
/* NaN, the infinities and -0 have no JSON spelling, so the protocol sends
|
|
323
|
+
them by name instead of as a value. Every other number is just a
|
|
324
|
+
number: adding a description to those would be noise. */
|
|
325
|
+
const char *name = NULL;
|
|
326
|
+
if (isnan(d))
|
|
327
|
+
name = "NaN";
|
|
328
|
+
else if (isinf(d))
|
|
329
|
+
name = d > 0 ? "Infinity" : "-Infinity";
|
|
330
|
+
else if (d == 0 && signbit(d))
|
|
331
|
+
name = "-0";
|
|
332
|
+
if (name) {
|
|
333
|
+
json_set_string(agent, o, "unserializableValue", name);
|
|
334
|
+
json_set_string(agent, o, "description", name);
|
|
335
|
+
} else {
|
|
336
|
+
json_set(agent, o, "value", JS_NewFloat64(agent->jctx, d));
|
|
337
|
+
}
|
|
338
|
+
return o;
|
|
339
|
+
}
|
|
340
|
+
if (JS_IsString(v)) {
|
|
341
|
+
const char *s = debuggee_to_string(agent, v);
|
|
342
|
+
json_set_string(agent, o, "type", "string");
|
|
343
|
+
json_set_string(agent, o, "value", s);
|
|
344
|
+
free_debuggee_string(agent, s);
|
|
345
|
+
return o;
|
|
346
|
+
}
|
|
347
|
+
if (JS_IsBigInt(v)) {
|
|
348
|
+
const char *s = debuggee_to_string(agent, v);
|
|
349
|
+
json_set_string(agent, o, "type", "bigint");
|
|
350
|
+
json_set_string(agent, o, "description", s);
|
|
351
|
+
free_debuggee_string(agent, s);
|
|
352
|
+
return o;
|
|
353
|
+
}
|
|
354
|
+
if (JS_IsSymbol(v)) {
|
|
355
|
+
const char *s = debuggee_to_string(agent, v);
|
|
356
|
+
json_set_string(agent, o, "type", "symbol");
|
|
357
|
+
json_set_string(agent, o, "description", s);
|
|
358
|
+
free_debuggee_string(agent, s);
|
|
359
|
+
char sid[16];
|
|
360
|
+
snprintf(sid, sizeof(sid), "%d", remember_object(agent, v, group));
|
|
361
|
+
json_set_string(agent, o, "objectId", sid);
|
|
362
|
+
return o;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
bool is_fn = JS_IsFunction(agent->ctx, v);
|
|
366
|
+
json_set_string(agent, o, "type", is_fn ? "function" : "object");
|
|
367
|
+
|
|
368
|
+
const char *sub = subtype_name(agent, v);
|
|
369
|
+
if (sub) json_set_string(agent, o, "subtype", sub);
|
|
370
|
+
|
|
371
|
+
/* className comes from the constructor rather than from toString, which for
|
|
372
|
+
a plain object is "[object Object]" and tells the frontend nothing. */
|
|
373
|
+
JSValue ctor = JS_GetPropertyStr(agent->ctx, v, "constructor");
|
|
374
|
+
JSValue name = JS_IsObject(ctor) ? JS_GetPropertyStr(agent->ctx, ctor, "name")
|
|
375
|
+
: JS_UNDEFINED;
|
|
376
|
+
const char *cn = JS_IsString(name) ? debuggee_to_string(agent, name) : NULL;
|
|
377
|
+
json_set_string(agent, o, "className", cn && *cn ? cn : "Object");
|
|
378
|
+
if (cn) free_debuggee_string(agent, cn);
|
|
379
|
+
JS_FreeValue(agent->ctx, name);
|
|
380
|
+
JS_FreeValue(agent->ctx, ctor);
|
|
381
|
+
|
|
382
|
+
set_description(agent, o, v);
|
|
383
|
+
|
|
384
|
+
char id[16];
|
|
385
|
+
snprintf(id, sizeof(id), "%d", remember_object(agent, v, group));
|
|
386
|
+
json_set_string(agent, o, "objectId", id);
|
|
387
|
+
return o;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Takes ownership of exc. */
|
|
391
|
+
static JSValue to_exception_details(QJSCDPAgent *agent, JSValue exc) {
|
|
392
|
+
JSValue d = json_object(agent);
|
|
393
|
+
json_set_int(agent, d, "exceptionId", 1);
|
|
394
|
+
json_set_int(agent, d, "lineNumber", 0);
|
|
395
|
+
json_set_int(agent, d, "columnNumber", 0);
|
|
396
|
+
|
|
397
|
+
const char *text = debuggee_to_string(agent, exc);
|
|
398
|
+
json_set_string(agent, d, "text", text ? text : "Uncaught");
|
|
399
|
+
if (text) free_debuggee_string(agent, text);
|
|
400
|
+
|
|
401
|
+
json_set(agent, d, "exception", to_remote_object(agent, exc, NULL));
|
|
402
|
+
JS_FreeValue(agent->ctx, exc);
|
|
403
|
+
return d;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* ------------------------------------------------------------- scripts --- */
|
|
407
|
+
|
|
408
|
+
static Script *find_script_by_url(QJSCDPAgent *agent, const char *url) {
|
|
409
|
+
if (!url) return NULL;
|
|
410
|
+
for (int i = 0; i < agent->nscripts; i++)
|
|
411
|
+
if (strcmp(agent->scripts[i].url, url) == 0) return &agent->scripts[i];
|
|
412
|
+
return NULL;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
static Script *find_script_by_id(QJSCDPAgent *agent, const char *script_id) {
|
|
416
|
+
if (!script_id) return NULL;
|
|
417
|
+
int id = atoi(script_id);
|
|
418
|
+
for (int i = 0; i < agent->nscripts; i++)
|
|
419
|
+
if (agent->scripts[i].id == id) return &agent->scripts[i];
|
|
420
|
+
return NULL;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* A breakpoint URL matches a script whose URL ends with it, so that a frontend
|
|
424
|
+
that only knows "app.js" still binds against "http://localhost/app.js". */
|
|
425
|
+
static bool url_matches(const char *script_url, const char *wanted) {
|
|
426
|
+
size_t n = strlen(script_url), m = strlen(wanted);
|
|
427
|
+
return m <= n && strcmp(script_url + n - m, wanted) == 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
static void announce_script(
|
|
431
|
+
QJSCDPAgent *agent, const Script *script, bool everyone) {
|
|
432
|
+
char id[16];
|
|
433
|
+
snprintf(id, sizeof(id), "%d", script->id);
|
|
434
|
+
|
|
435
|
+
int lines = 0;
|
|
436
|
+
for (const char *p = script->source; *p; p++)
|
|
437
|
+
if (*p == '\n') lines++;
|
|
438
|
+
|
|
439
|
+
JSValue p = json_object(agent);
|
|
440
|
+
json_set_string(agent, p, "scriptId", id);
|
|
441
|
+
json_set_string(agent, p, "url", script->url);
|
|
442
|
+
json_set_int(agent, p, "startLine", 0);
|
|
443
|
+
json_set_int(agent, p, "startColumn", 0);
|
|
444
|
+
json_set_int(agent, p, "endLine", lines);
|
|
445
|
+
json_set_int(agent, p, "endColumn", 0);
|
|
446
|
+
json_set_int(agent, p, "executionContextId", agent->exec_ctx_id);
|
|
447
|
+
json_set_string(agent, p, "hash", id);
|
|
448
|
+
send_event_to(agent, "Debugger.scriptParsed", p, everyone);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* --------------------------------------------------------- breakpoints --- */
|
|
452
|
+
|
|
453
|
+
static void announce_breakpoint(
|
|
454
|
+
QJSCDPAgent *agent, const Breakpoint *bp, const Script *script) {
|
|
455
|
+
char bid[16], sid[16];
|
|
456
|
+
snprintf(bid, sizeof(bid), "%d", bp->id);
|
|
457
|
+
snprintf(sid, sizeof(sid), "%d", script->id);
|
|
458
|
+
|
|
459
|
+
JSValue loc = json_object(agent);
|
|
460
|
+
json_set_string(agent, loc, "scriptId", sid);
|
|
461
|
+
json_set_int(agent, loc, "lineNumber", bp->line - 1);
|
|
462
|
+
json_set_int(agent, loc, "columnNumber", bp->col > 0 ? bp->col - 1 : 0);
|
|
463
|
+
|
|
464
|
+
JSValue p = json_object(agent);
|
|
465
|
+
json_set_string(agent, p, "breakpointId", bid);
|
|
466
|
+
json_set(agent, p, "location", loc);
|
|
467
|
+
send_event(agent, "Debugger.breakpointResolved", p);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Binds every breakpoint that named this script's URL but had no script to
|
|
471
|
+
attach to when it was set. */
|
|
472
|
+
static void resolve_breakpoints(QJSCDPAgent *agent, const Script *script) {
|
|
473
|
+
for (int i = 0; i < agent->nbps; i++) {
|
|
474
|
+
Breakpoint *bp = &agent->bps[i];
|
|
475
|
+
if (bp->resolved || !url_matches(script->url, bp->url)) continue;
|
|
476
|
+
bp->resolved = true;
|
|
477
|
+
announce_breakpoint(agent, bp, script);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
static Breakpoint *breakpoint_at(
|
|
482
|
+
QJSCDPAgent *agent, const char *file, int line) {
|
|
483
|
+
if (!agent->breakpoints_active || !file) return NULL;
|
|
484
|
+
for (int i = 0; i < agent->nbps; i++)
|
|
485
|
+
if (agent->bps[i].line == line && url_matches(file, agent->bps[i].url))
|
|
486
|
+
return &agent->bps[i];
|
|
487
|
+
return NULL;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* --------------------------------------------------------- call frames --- */
|
|
491
|
+
|
|
492
|
+
/* The frame's arguments, locals and captured variables, as one debuggee object
|
|
493
|
+
the frontend can expand. */
|
|
494
|
+
static JSValue frame_scope_object(QJSCDPAgent *agent, int level) {
|
|
495
|
+
JSDebugLocalVar *vars = NULL;
|
|
496
|
+
int count = 0;
|
|
497
|
+
if (JS_GetLocalVariablesAtLevel(agent->ctx, level, &vars, &count) < 0)
|
|
498
|
+
return JS_NewObject(agent->ctx);
|
|
499
|
+
|
|
500
|
+
JSValue scope = JS_NewObject(agent->ctx);
|
|
501
|
+
for (int i = 0; i < count; i++)
|
|
502
|
+
JS_SetPropertyStr(
|
|
503
|
+
agent->ctx, scope, vars[i].name,
|
|
504
|
+
JS_DupValue(agent->ctx, vars[i].value));
|
|
505
|
+
JS_FreeLocalVariables(agent->ctx, vars, count);
|
|
506
|
+
return scope;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* quickjs names the top-level frame "<eval>"; the protocol calls it "global". */
|
|
510
|
+
static const char *frame_name(const char *func) {
|
|
511
|
+
if (!func || !*func) return "";
|
|
512
|
+
return strcmp(func, "<eval>") == 0 ? "global" : func;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
static JSValue call_frames(QJSCDPAgent *agent) {
|
|
516
|
+
JSValue frames = json_array(agent);
|
|
517
|
+
int depth = JS_GetStackDepth(agent->ctx);
|
|
518
|
+
uint32_t n = 0;
|
|
519
|
+
|
|
520
|
+
for (int level = 0; level < depth; level++) {
|
|
521
|
+
JSDebugFrameInfo info;
|
|
522
|
+
if (JS_GetFrameInfoAtLevel(agent->ctx, level, &info) < 0) break;
|
|
523
|
+
if (info.is_native) continue;
|
|
524
|
+
|
|
525
|
+
const char *file = info.filename != JS_ATOM_NULL
|
|
526
|
+
? JS_AtomToCString(agent->ctx, info.filename)
|
|
527
|
+
: NULL;
|
|
528
|
+
const char *func = info.func_name != JS_ATOM_NULL
|
|
529
|
+
? JS_AtomToCString(agent->ctx, info.func_name)
|
|
530
|
+
: NULL;
|
|
531
|
+
Script *script = file ? find_script_by_url(agent, file) : NULL;
|
|
532
|
+
|
|
533
|
+
char frame_id[16], script_id[16];
|
|
534
|
+
snprintf(frame_id, sizeof(frame_id), "%d", level);
|
|
535
|
+
snprintf(script_id, sizeof(script_id), "%d", script ? script->id : 0);
|
|
536
|
+
|
|
537
|
+
/* CDP counts lines and columns from zero; the engine counts from one. */
|
|
538
|
+
JSValue location = json_object(agent);
|
|
539
|
+
json_set_string(agent, location, "scriptId", script_id);
|
|
540
|
+
json_set_int(
|
|
541
|
+
agent, location, "lineNumber", info.line > 0 ? info.line - 1 : 0);
|
|
542
|
+
json_set_int(
|
|
543
|
+
agent, location, "columnNumber", info.col > 0 ? info.col - 1 : 0);
|
|
544
|
+
|
|
545
|
+
JSValue locals = frame_scope_object(agent, level);
|
|
546
|
+
JSValue scope = json_object(agent);
|
|
547
|
+
json_set_string(agent, scope, "type", "local");
|
|
548
|
+
json_set(
|
|
549
|
+
agent, scope, "object", to_remote_object(agent, locals, "backtrace"));
|
|
550
|
+
JS_FreeValue(agent->ctx, locals);
|
|
551
|
+
|
|
552
|
+
JSValue scope_chain = json_array(agent);
|
|
553
|
+
json_set_index(agent, scope_chain, 0, scope);
|
|
554
|
+
|
|
555
|
+
JSValue frame = json_object(agent);
|
|
556
|
+
json_set_string(agent, frame, "callFrameId", frame_id);
|
|
557
|
+
json_set_string(agent, frame, "functionName", frame_name(func));
|
|
558
|
+
json_set(agent, frame, "location", location);
|
|
559
|
+
json_set(agent, frame, "url", JS_NewString(agent->jctx, file ? file : ""));
|
|
560
|
+
json_set(agent, frame, "scopeChain", scope_chain);
|
|
561
|
+
json_set(agent, frame, "this", to_remote_object(agent, JS_UNDEFINED, NULL));
|
|
562
|
+
json_set_index(agent, frames, n++, frame);
|
|
563
|
+
|
|
564
|
+
if (file) JS_FreeCString(agent->ctx, file);
|
|
565
|
+
if (func) JS_FreeCString(agent->ctx, func);
|
|
566
|
+
}
|
|
567
|
+
return frames;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ------------------------------------------------------------- pausing --- */
|
|
571
|
+
|
|
572
|
+
static void handle_message(QJSCDPAgent *agent, const char *json, size_t len);
|
|
573
|
+
|
|
574
|
+
/* Caller holds the lock. */
|
|
575
|
+
/*
|
|
576
|
+
* The agent's own runtime is entered from whichever thread is currently
|
|
577
|
+
* driving the agent, and the engine records the stack bounds of the thread
|
|
578
|
+
* that created a runtime. React Native creates the agent on one thread and
|
|
579
|
+
* then calls it from the JS thread; the pause loop runs on the JS thread while
|
|
580
|
+
* the frontend posts from another.
|
|
581
|
+
*
|
|
582
|
+
* Without this, every use of the agent's runtime from a second thread fails
|
|
583
|
+
* the stack check on its first allocation -- so a posted Debugger.resume never
|
|
584
|
+
* parses, the pause loop never wakes, and the app hangs at the breakpoint.
|
|
585
|
+
* Both thread stacks happen to sit close together on macOS, so it only shows
|
|
586
|
+
* up on Linux and Android.
|
|
587
|
+
*/
|
|
588
|
+
static void enter_agent_runtime(QJSCDPAgent *agent) {
|
|
589
|
+
JS_UpdateStackTop(agent->jrt);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
static char *take_message(QJSCDPAgent *agent, void **session) {
|
|
593
|
+
if (agent->queue_len == 0) return NULL;
|
|
594
|
+
char *msg = agent->queue[0];
|
|
595
|
+
*session = agent->queue_session[0];
|
|
596
|
+
agent->queue_len--;
|
|
597
|
+
memmove(
|
|
598
|
+
agent->queue, agent->queue + 1,
|
|
599
|
+
sizeof(char *) * (size_t)agent->queue_len);
|
|
600
|
+
memmove(
|
|
601
|
+
agent->queue_session, agent->queue_session + 1,
|
|
602
|
+
sizeof(void *) * (size_t)agent->queue_len);
|
|
603
|
+
return msg;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* Stops the JS thread here and answers the frontend from inside the engine
|
|
607
|
+
until it is told to continue. This is the only reason the message queue
|
|
608
|
+
exists: while this loop is running the JS thread cannot return to whatever
|
|
609
|
+
would otherwise be pumping it. */
|
|
610
|
+
static void pause_until_resumed(
|
|
611
|
+
QJSCDPAgent *agent, const char *reason, int breakpoint_id) {
|
|
612
|
+
agent->paused = true;
|
|
613
|
+
agent->resumed = false;
|
|
614
|
+
agent->pause_pending = false;
|
|
615
|
+
agent->mode = MODE_RUN;
|
|
616
|
+
|
|
617
|
+
JSValue params = json_object(agent);
|
|
618
|
+
json_set(agent, params, "callFrames", call_frames(agent));
|
|
619
|
+
json_set_string(agent, params, "reason", reason);
|
|
620
|
+
if (breakpoint_id > 0) {
|
|
621
|
+
char bid[16];
|
|
622
|
+
snprintf(bid, sizeof(bid), "%d", breakpoint_id);
|
|
623
|
+
JSValue hit = json_array(agent);
|
|
624
|
+
json_set_index(agent, hit, 0, JS_NewString(agent->jctx, bid));
|
|
625
|
+
json_set(agent, params, "hitBreakpoints", hit);
|
|
626
|
+
}
|
|
627
|
+
send_event(agent, "Debugger.paused", params);
|
|
628
|
+
|
|
629
|
+
pthread_mutex_lock(&agent->lock);
|
|
630
|
+
for (;;) {
|
|
631
|
+
void *session = NULL;
|
|
632
|
+
char *msg = take_message(agent, &session);
|
|
633
|
+
if (msg) {
|
|
634
|
+
pthread_mutex_unlock(&agent->lock);
|
|
635
|
+
agent->session = session;
|
|
636
|
+
handle_message(agent, msg, strlen(msg));
|
|
637
|
+
agent->session = NULL;
|
|
638
|
+
free(msg);
|
|
639
|
+
pthread_mutex_lock(&agent->lock);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (agent->resumed) break;
|
|
643
|
+
pthread_cond_wait(&agent->cond, &agent->lock);
|
|
644
|
+
}
|
|
645
|
+
pthread_mutex_unlock(&agent->lock);
|
|
646
|
+
|
|
647
|
+
agent->paused = false;
|
|
648
|
+
send_event(agent, "Debugger.resumed", json_object(agent));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
static int on_statement(
|
|
652
|
+
JSContext *ctx, JSAtom filename, JSAtom funcname, int line, int col,
|
|
653
|
+
int flags, void *opaque) {
|
|
654
|
+
(void)funcname;
|
|
655
|
+
(void)col;
|
|
656
|
+
QJSCDPAgent *agent = opaque;
|
|
657
|
+
enter_agent_runtime(agent);
|
|
658
|
+
|
|
659
|
+
/* Re-entrant: an evaluate issued from the pause loop runs instrumented code
|
|
660
|
+
of its own, and stopping inside it would deadlock the frontend. */
|
|
661
|
+
if (agent->paused || !agent->debugger_enabled) return 0;
|
|
662
|
+
|
|
663
|
+
const char *reason = NULL;
|
|
664
|
+
int breakpoint_id = 0;
|
|
665
|
+
int depth = JS_GetStackDepth(ctx);
|
|
666
|
+
|
|
667
|
+
if (agent->pause_pending) {
|
|
668
|
+
reason = "other";
|
|
669
|
+
} else if (flags & JS_DEBUG_TRACE_DEBUGGER_STMT) {
|
|
670
|
+
reason = "debuggerStatement";
|
|
671
|
+
} else if (agent->mode == MODE_PAUSE_NEXT) {
|
|
672
|
+
reason = "other";
|
|
673
|
+
} else if (agent->mode == MODE_STEP_INTO) {
|
|
674
|
+
reason = "step";
|
|
675
|
+
} else if (agent->mode == MODE_STEP_OVER && depth <= agent->step_depth) {
|
|
676
|
+
reason = "step";
|
|
677
|
+
} else if (agent->mode == MODE_STEP_OUT && depth < agent->step_depth) {
|
|
678
|
+
reason = "step";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!reason && filename != JS_ATOM_NULL) {
|
|
682
|
+
const char *file = JS_AtomToCString(ctx, filename);
|
|
683
|
+
Breakpoint *bp = breakpoint_at(agent, file, line);
|
|
684
|
+
if (bp) {
|
|
685
|
+
reason = "other";
|
|
686
|
+
breakpoint_id = bp->id;
|
|
687
|
+
}
|
|
688
|
+
JS_FreeCString(ctx, file);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (reason) pause_until_resumed(agent, reason, breakpoint_id);
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
static void resume_with(QJSCDPAgent *agent, PauseMode mode) {
|
|
696
|
+
agent->mode = mode;
|
|
697
|
+
agent->step_depth = JS_GetStackDepth(agent->ctx);
|
|
698
|
+
pthread_mutex_lock(&agent->lock);
|
|
699
|
+
agent->resumed = true;
|
|
700
|
+
pthread_cond_broadcast(&agent->cond);
|
|
701
|
+
pthread_mutex_unlock(&agent->lock);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/* ------------------------------------------------------------- methods --- */
|
|
705
|
+
|
|
706
|
+
/* Both engines can already read and write JSON, so a value crosses between
|
|
707
|
+
them as text rather than through a converter of our own. */
|
|
708
|
+
static JSValue protocol_value_to_debuggee(QJSCDPAgent *agent, JSValue v) {
|
|
709
|
+
JSValue text = JS_JSONStringify(agent->jctx, v, JS_UNDEFINED, JS_UNDEFINED);
|
|
710
|
+
size_t len = 0;
|
|
711
|
+
const char *s = JS_ToCStringLen(agent->jctx, &len, text);
|
|
712
|
+
JSValue out = JS_UNDEFINED;
|
|
713
|
+
if (s) {
|
|
714
|
+
out = JS_ParseJSON(agent->ctx, s, len, "<argument>");
|
|
715
|
+
if (JS_IsException(out)) {
|
|
716
|
+
JS_FreeValue(agent->ctx, JS_GetException(agent->ctx));
|
|
717
|
+
out = JS_UNDEFINED;
|
|
718
|
+
}
|
|
719
|
+
JS_FreeCString(agent->jctx, s);
|
|
720
|
+
}
|
|
721
|
+
JS_FreeValue(agent->jctx, text);
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/* Takes ownership of value. Answers with the CDP shape shared by
|
|
726
|
+
Runtime.evaluate and Debugger.evaluateOnCallFrame. */
|
|
727
|
+
static void send_evaluation(
|
|
728
|
+
QJSCDPAgent *agent, int id, JSValue value, const char *group) {
|
|
729
|
+
JSValue result = json_object(agent);
|
|
730
|
+
if (JS_IsException(value)) {
|
|
731
|
+
JSValue exc = JS_GetException(agent->ctx);
|
|
732
|
+
json_set(agent, result, "result", to_remote_object(agent, exc, group));
|
|
733
|
+
json_set(
|
|
734
|
+
agent, result, "exceptionDetails",
|
|
735
|
+
to_exception_details(agent, JS_DupValue(agent->ctx, exc)));
|
|
736
|
+
JS_FreeValue(agent->ctx, exc);
|
|
737
|
+
} else {
|
|
738
|
+
json_set(agent, result, "result", to_remote_object(agent, value, group));
|
|
739
|
+
}
|
|
740
|
+
JS_FreeValue(agent->ctx, value);
|
|
741
|
+
send_result(agent, id, result);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
static void send_properties(QJSCDPAgent *agent, int id, JSValue params) {
|
|
745
|
+
const char *object_id = json_get_string(agent, params, "objectId");
|
|
746
|
+
RemoteObject *remote = find_object(agent, object_id);
|
|
747
|
+
if (object_id) JS_FreeCString(agent->jctx, object_id);
|
|
748
|
+
if (!remote) {
|
|
749
|
+
send_error(agent, id, "No object found for the given objectId");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* Not JS_GPN_ENUM_ONLY: the frontend asked for own properties and expects the
|
|
754
|
+
non-enumerable ones too, which is how an empty array still has a `length`
|
|
755
|
+
to show. */
|
|
756
|
+
JSPropertyEnum *props = NULL;
|
|
757
|
+
uint32_t count = 0;
|
|
758
|
+
if (JS_GetOwnPropertyNames(
|
|
759
|
+
agent->ctx, &props, &count, remote->value, JS_GPN_STRING_MASK) < 0) {
|
|
760
|
+
JS_FreeValue(agent->ctx, JS_GetException(agent->ctx));
|
|
761
|
+
send_error(agent, id, "Could not read the object's properties");
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
JSValue list = json_array(agent);
|
|
766
|
+
for (uint32_t i = 0; i < count; i++) {
|
|
767
|
+
const char *name = JS_AtomToCString(agent->ctx, props[i].atom);
|
|
768
|
+
|
|
769
|
+
JSPropertyDescriptor desc;
|
|
770
|
+
int described =
|
|
771
|
+
JS_GetOwnProperty(agent->ctx, &desc, remote->value, props[i].atom);
|
|
772
|
+
if (described < 0) JS_FreeValue(agent->ctx, JS_GetException(agent->ctx));
|
|
773
|
+
|
|
774
|
+
JSValue entry = json_object(agent);
|
|
775
|
+
json_set_string(agent, entry, "name", name ? name : "");
|
|
776
|
+
json_set_bool(agent, entry, "isOwn", true);
|
|
777
|
+
json_set_bool(
|
|
778
|
+
agent, entry, "writable",
|
|
779
|
+
described > 0 && (desc.flags & JS_PROP_WRITABLE) != 0);
|
|
780
|
+
json_set_bool(
|
|
781
|
+
agent, entry, "configurable",
|
|
782
|
+
described > 0 && (desc.flags & JS_PROP_CONFIGURABLE) != 0);
|
|
783
|
+
json_set_bool(
|
|
784
|
+
agent, entry, "enumerable",
|
|
785
|
+
described > 0 && (desc.flags & JS_PROP_ENUMERABLE) != 0);
|
|
786
|
+
|
|
787
|
+
if (described > 0 && (desc.flags & JS_PROP_GETSET)) {
|
|
788
|
+
/* An accessor is described, not invoked. Reading it to fill in a value
|
|
789
|
+
would run the program's own code to answer a question about it. */
|
|
790
|
+
if (!JS_IsUndefined(desc.getter))
|
|
791
|
+
json_set(
|
|
792
|
+
agent, entry, "get", to_remote_object(agent, desc.getter, NULL));
|
|
793
|
+
if (!JS_IsUndefined(desc.setter))
|
|
794
|
+
json_set(
|
|
795
|
+
agent, entry, "set", to_remote_object(agent, desc.setter, NULL));
|
|
796
|
+
} else if (described > 0) {
|
|
797
|
+
json_set(
|
|
798
|
+
agent, entry, "value", to_remote_object(agent, desc.value, NULL));
|
|
799
|
+
}
|
|
800
|
+
if (described > 0) {
|
|
801
|
+
JS_FreeValue(agent->ctx, desc.value);
|
|
802
|
+
JS_FreeValue(agent->ctx, desc.getter);
|
|
803
|
+
JS_FreeValue(agent->ctx, desc.setter);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
json_set_index(agent, list, i, entry);
|
|
807
|
+
if (name) JS_FreeCString(agent->ctx, name);
|
|
808
|
+
}
|
|
809
|
+
JS_FreePropertyEnum(agent->ctx, props, count);
|
|
810
|
+
|
|
811
|
+
JSValue result = json_object(agent);
|
|
812
|
+
json_set(agent, result, "result", list);
|
|
813
|
+
send_result(agent, id, result);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
static void set_breakpoint_by_url(QJSCDPAgent *agent, int id, JSValue params) {
|
|
817
|
+
const char *url = json_get_string(agent, params, "url");
|
|
818
|
+
if (!url) {
|
|
819
|
+
send_error(agent, id, "Debugger.setBreakpointByUrl needs a url");
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const char *condition = json_get_string(agent, params, "condition");
|
|
823
|
+
|
|
824
|
+
agent->bps = realloc(agent->bps, sizeof(Breakpoint) * (agent->nbps + 1));
|
|
825
|
+
Breakpoint *bp = &agent->bps[agent->nbps++];
|
|
826
|
+
bp->id = ++agent->next_bp_id;
|
|
827
|
+
bp->url = strdup(url);
|
|
828
|
+
bp->condition = condition && *condition ? strdup(condition) : NULL;
|
|
829
|
+
bp->line = json_get_int(agent, params, "lineNumber", 0) + 1;
|
|
830
|
+
bp->col = json_get_int(agent, params, "columnNumber", 0) + 1;
|
|
831
|
+
bp->resolved = false;
|
|
832
|
+
|
|
833
|
+
char bid[16];
|
|
834
|
+
snprintf(bid, sizeof(bid), "%d", bp->id);
|
|
835
|
+
JSValue locations = json_array(agent);
|
|
836
|
+
|
|
837
|
+
Script *script = NULL;
|
|
838
|
+
for (int i = 0; i < agent->nscripts; i++)
|
|
839
|
+
if (url_matches(agent->scripts[i].url, url)) script = &agent->scripts[i];
|
|
840
|
+
|
|
841
|
+
if (script) {
|
|
842
|
+
bp->resolved = true;
|
|
843
|
+
char sid[16];
|
|
844
|
+
snprintf(sid, sizeof(sid), "%d", script->id);
|
|
845
|
+
JSValue loc = json_object(agent);
|
|
846
|
+
json_set_string(agent, loc, "scriptId", sid);
|
|
847
|
+
json_set_int(agent, loc, "lineNumber", bp->line - 1);
|
|
848
|
+
json_set_int(agent, loc, "columnNumber", bp->col - 1);
|
|
849
|
+
json_set_index(agent, locations, 0, loc);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
JSValue result = json_object(agent);
|
|
853
|
+
json_set_string(agent, result, "breakpointId", bid);
|
|
854
|
+
json_set(agent, result, "locations", locations);
|
|
855
|
+
send_result(agent, id, result);
|
|
856
|
+
|
|
857
|
+
JS_FreeCString(agent->jctx, url);
|
|
858
|
+
if (condition) JS_FreeCString(agent->jctx, condition);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
static void remove_breakpoint(QJSCDPAgent *agent, JSValue params) {
|
|
862
|
+
const char *bid = json_get_string(agent, params, "breakpointId");
|
|
863
|
+
if (!bid) return;
|
|
864
|
+
int wanted = atoi(bid);
|
|
865
|
+
for (int i = 0; i < agent->nbps; i++) {
|
|
866
|
+
if (agent->bps[i].id != wanted) continue;
|
|
867
|
+
free(agent->bps[i].url);
|
|
868
|
+
free(agent->bps[i].condition);
|
|
869
|
+
agent->bps[i] = agent->bps[--agent->nbps];
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
JS_FreeCString(agent->jctx, bid);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* Writes a local in a frame that is stopped, so that the frontend's "edit
|
|
876
|
+
value" reaches the live frame rather than a copy of it. */
|
|
877
|
+
static void set_variable_value(QJSCDPAgent *agent, int id, JSValue params) {
|
|
878
|
+
const char *frame_id = json_get_string(agent, params, "callFrameId");
|
|
879
|
+
const char *name = json_get_string(agent, params, "variableName");
|
|
880
|
+
if (!frame_id || !name) {
|
|
881
|
+
send_error(
|
|
882
|
+
agent, id, "Debugger.setVariableValue needs a call frame and a name");
|
|
883
|
+
goto done;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
JSValue arg = json_get(agent, params, "newValue");
|
|
887
|
+
JSValue wanted = json_get(agent, arg, "value");
|
|
888
|
+
JSValue value = protocol_value_to_debuggee(agent, wanted);
|
|
889
|
+
JS_FreeValue(agent->jctx, wanted);
|
|
890
|
+
JS_FreeValue(agent->jctx, arg);
|
|
891
|
+
|
|
892
|
+
int rc = JS_SetVariableAtLevel(agent->ctx, atoi(frame_id), name, value);
|
|
893
|
+
if (rc == 0)
|
|
894
|
+
send_empty_result(agent, id);
|
|
895
|
+
else if (rc == -2)
|
|
896
|
+
send_error(agent, id, "That binding is a constant");
|
|
897
|
+
else
|
|
898
|
+
send_error(agent, id, "No such variable in that call frame");
|
|
899
|
+
|
|
900
|
+
done:
|
|
901
|
+
if (frame_id) JS_FreeCString(agent->jctx, frame_id);
|
|
902
|
+
if (name) JS_FreeCString(agent->jctx, name);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/* ------------------------------------------------------------ dispatch --- */
|
|
906
|
+
|
|
907
|
+
void qjs_cdp_set_debugger_enabled(QJSCDPAgent *agent, bool enabled);
|
|
908
|
+
|
|
909
|
+
static bool is(const char *method, const char *name) {
|
|
910
|
+
return strcmp(method, name) == 0;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/* Every method dispatch() answers. Kept beside it rather than derived from it
|
|
914
|
+
because the frontend needs the answer before the message is queued, on
|
|
915
|
+
another thread; CDPTest.EveryAdvertisedMethodIsImplemented keeps the two
|
|
916
|
+
honest. */
|
|
917
|
+
static const char *const kMethods[] = {
|
|
918
|
+
"Runtime.evaluate",
|
|
919
|
+
"Runtime.getProperties",
|
|
920
|
+
"Runtime.releaseObject",
|
|
921
|
+
"Runtime.releaseObjectGroup",
|
|
922
|
+
"Runtime.getHeapUsage",
|
|
923
|
+
"Runtime.discardConsoleEntries",
|
|
924
|
+
"Debugger.enable",
|
|
925
|
+
"Debugger.disable",
|
|
926
|
+
"Debugger.setBreakpointByUrl",
|
|
927
|
+
"Debugger.removeBreakpoint",
|
|
928
|
+
"Debugger.setBreakpointsActive",
|
|
929
|
+
"Debugger.resume",
|
|
930
|
+
"Debugger.stepOver",
|
|
931
|
+
"Debugger.stepInto",
|
|
932
|
+
"Debugger.stepOut",
|
|
933
|
+
"Debugger.pause",
|
|
934
|
+
"Debugger.evaluateOnCallFrame",
|
|
935
|
+
"Debugger.setVariableValue",
|
|
936
|
+
"Debugger.getScriptSource",
|
|
937
|
+
"Debugger.setPauseOnExceptions",
|
|
938
|
+
"Debugger.setAsyncCallStackDepth",
|
|
939
|
+
"Debugger.setBlackboxPatterns",
|
|
940
|
+
"Debugger.setBlackboxedRanges",
|
|
941
|
+
"Debugger.setSkipAllPauses",
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
bool qjs_cdp_handles(const char *method) {
|
|
945
|
+
if (!method) return false;
|
|
946
|
+
for (size_t i = 0; i < sizeof(kMethods) / sizeof(kMethods[0]); i++)
|
|
947
|
+
if (is(method, kMethods[i])) return true;
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
static void dispatch(
|
|
952
|
+
QJSCDPAgent *agent, int id, const char *method, JSValue params) {
|
|
953
|
+
/* --- Runtime --- */
|
|
954
|
+
if (is(method, "Runtime.evaluate")) {
|
|
955
|
+
/* A contextId naming some other execution context is an invalid request,
|
|
956
|
+
not an evaluation: answering it would run the expression in the wrong
|
|
957
|
+
place and the frontend would never know. */
|
|
958
|
+
const int wanted =
|
|
959
|
+
json_get_int(agent, params, "contextId", agent->exec_ctx_id);
|
|
960
|
+
if (wanted != agent->exec_ctx_id) {
|
|
961
|
+
send_error_code(agent, id, -32600, "Unknown execution context id");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const char *expr = json_get_string(agent, params, "expression");
|
|
965
|
+
const char *group = json_get_string(agent, params, "objectGroup");
|
|
966
|
+
JSValue v = expr ? JS_Eval(
|
|
967
|
+
agent->ctx, expr, strlen(expr), "<evaluate>",
|
|
968
|
+
JS_EVAL_TYPE_GLOBAL)
|
|
969
|
+
: JS_UNDEFINED;
|
|
970
|
+
send_evaluation(agent, id, v, group);
|
|
971
|
+
if (expr) JS_FreeCString(agent->jctx, expr);
|
|
972
|
+
if (group) JS_FreeCString(agent->jctx, group);
|
|
973
|
+
|
|
974
|
+
} else if (is(method, "Runtime.getProperties")) {
|
|
975
|
+
send_properties(agent, id, params);
|
|
976
|
+
|
|
977
|
+
} else if (is(method, "Runtime.releaseObject")) {
|
|
978
|
+
const char *object_id = json_get_string(agent, params, "objectId");
|
|
979
|
+
const bool released = release_object(agent, object_id);
|
|
980
|
+
if (object_id) JS_FreeCString(agent->jctx, object_id);
|
|
981
|
+
/* Releasing something already released is an error, not a no-op: the
|
|
982
|
+
frontend is telling us about a reference it believes it holds. */
|
|
983
|
+
if (released)
|
|
984
|
+
send_empty_result(agent, id);
|
|
985
|
+
else
|
|
986
|
+
send_error(agent, id, "No object found for the given objectId");
|
|
987
|
+
|
|
988
|
+
} else if (is(method, "Runtime.releaseObjectGroup")) {
|
|
989
|
+
const char *group = json_get_string(agent, params, "objectGroup");
|
|
990
|
+
release_object_group(agent, group);
|
|
991
|
+
if (group) JS_FreeCString(agent->jctx, group);
|
|
992
|
+
send_empty_result(agent, id);
|
|
993
|
+
|
|
994
|
+
} else if (is(method, "Runtime.getHeapUsage")) {
|
|
995
|
+
JSMemoryUsage usage;
|
|
996
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(agent->ctx), &usage);
|
|
997
|
+
JSValue result = json_object(agent);
|
|
998
|
+
json_set_int(agent, result, "usedSize", usage.memory_used_size);
|
|
999
|
+
json_set_int(agent, result, "totalSize", usage.malloc_size);
|
|
1000
|
+
send_result(agent, id, result);
|
|
1001
|
+
|
|
1002
|
+
} else if (
|
|
1003
|
+
is(method, "Runtime.discardConsoleEntries") ||
|
|
1004
|
+
is(method, "Runtime.setAsyncCallStackDepth") ||
|
|
1005
|
+
is(method, "Runtime.setCustomObjectFormatterEnabled") ||
|
|
1006
|
+
is(method, "Runtime.setMaxCallStackSizeToCapture")) {
|
|
1007
|
+
send_empty_result(agent, id);
|
|
1008
|
+
|
|
1009
|
+
/* --- Debugger --- */
|
|
1010
|
+
} else if (is(method, "Debugger.enable")) {
|
|
1011
|
+
/* Enabling is safe to act on directly -- one more frontend watching cannot
|
|
1012
|
+
hurt another. DISABLING is not, which is why it is the embedder that
|
|
1013
|
+
turns this back off, once it knows no session wants it any more. */
|
|
1014
|
+
qjs_cdp_set_debugger_enabled(agent, true);
|
|
1015
|
+
for (int i = 0; i < agent->nscripts; i++)
|
|
1016
|
+
announce_script(agent, &agent->scripts[i], false);
|
|
1017
|
+
send_empty_result(agent, id);
|
|
1018
|
+
|
|
1019
|
+
} else if (is(method, "Debugger.disable")) {
|
|
1020
|
+
send_empty_result(agent, id);
|
|
1021
|
+
|
|
1022
|
+
} else if (is(method, "Debugger.setBreakpointByUrl")) {
|
|
1023
|
+
set_breakpoint_by_url(agent, id, params);
|
|
1024
|
+
|
|
1025
|
+
} else if (is(method, "Debugger.removeBreakpoint")) {
|
|
1026
|
+
remove_breakpoint(agent, params);
|
|
1027
|
+
send_empty_result(agent, id);
|
|
1028
|
+
|
|
1029
|
+
} else if (is(method, "Debugger.setBreakpointsActive")) {
|
|
1030
|
+
agent->breakpoints_active = json_get_bool(agent, params, "active", true);
|
|
1031
|
+
send_empty_result(agent, id);
|
|
1032
|
+
|
|
1033
|
+
} else if (is(method, "Debugger.resume")) {
|
|
1034
|
+
send_empty_result(agent, id);
|
|
1035
|
+
resume_with(agent, MODE_RUN);
|
|
1036
|
+
|
|
1037
|
+
} else if (is(method, "Debugger.stepOver")) {
|
|
1038
|
+
send_empty_result(agent, id);
|
|
1039
|
+
resume_with(agent, MODE_STEP_OVER);
|
|
1040
|
+
|
|
1041
|
+
} else if (is(method, "Debugger.stepInto")) {
|
|
1042
|
+
send_empty_result(agent, id);
|
|
1043
|
+
resume_with(agent, MODE_STEP_INTO);
|
|
1044
|
+
|
|
1045
|
+
} else if (is(method, "Debugger.stepOut")) {
|
|
1046
|
+
send_empty_result(agent, id);
|
|
1047
|
+
resume_with(agent, MODE_STEP_OUT);
|
|
1048
|
+
|
|
1049
|
+
} else if (is(method, "Debugger.pause")) {
|
|
1050
|
+
agent->pause_pending = true;
|
|
1051
|
+
send_empty_result(agent, id);
|
|
1052
|
+
|
|
1053
|
+
} else if (is(method, "Debugger.evaluateOnCallFrame")) {
|
|
1054
|
+
const char *frame_id = json_get_string(agent, params, "callFrameId");
|
|
1055
|
+
const char *expr = json_get_string(agent, params, "expression");
|
|
1056
|
+
const char *group = json_get_string(agent, params, "objectGroup");
|
|
1057
|
+
JSValue v = (frame_id && expr) ? JS_EvalInStackFrame(
|
|
1058
|
+
agent->ctx, atoi(frame_id), expr,
|
|
1059
|
+
strlen(expr), "<evaluate>")
|
|
1060
|
+
: JS_UNDEFINED;
|
|
1061
|
+
send_evaluation(agent, id, v, group);
|
|
1062
|
+
if (frame_id) JS_FreeCString(agent->jctx, frame_id);
|
|
1063
|
+
if (expr) JS_FreeCString(agent->jctx, expr);
|
|
1064
|
+
if (group) JS_FreeCString(agent->jctx, group);
|
|
1065
|
+
|
|
1066
|
+
} else if (is(method, "Debugger.setVariableValue")) {
|
|
1067
|
+
set_variable_value(agent, id, params);
|
|
1068
|
+
|
|
1069
|
+
} else if (is(method, "Debugger.getScriptSource")) {
|
|
1070
|
+
const char *script_id = json_get_string(agent, params, "scriptId");
|
|
1071
|
+
Script *script = find_script_by_id(agent, script_id);
|
|
1072
|
+
if (script_id) JS_FreeCString(agent->jctx, script_id);
|
|
1073
|
+
if (!script) {
|
|
1074
|
+
send_error(agent, id, "No script with that scriptId");
|
|
1075
|
+
} else {
|
|
1076
|
+
JSValue result = json_object(agent);
|
|
1077
|
+
json_set_string(agent, result, "scriptSource", script->source);
|
|
1078
|
+
send_result(agent, id, result);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
} else if (
|
|
1082
|
+
is(method, "Debugger.setPauseOnExceptions") ||
|
|
1083
|
+
is(method, "Debugger.setAsyncCallStackDepth") ||
|
|
1084
|
+
is(method, "Debugger.setBlackboxPatterns") ||
|
|
1085
|
+
is(method, "Debugger.setBlackboxedRanges") ||
|
|
1086
|
+
is(method, "Debugger.setSkipAllPauses")) {
|
|
1087
|
+
send_empty_result(agent, id);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
static void handle_message(QJSCDPAgent *agent, const char *json, size_t len) {
|
|
1092
|
+
JSValue msg = JS_ParseJSON(agent->jctx, json, len, "<cdp>");
|
|
1093
|
+
if (JS_IsException(msg)) {
|
|
1094
|
+
JS_FreeValue(agent->jctx, JS_GetException(agent->jctx));
|
|
1095
|
+
JS_FreeValue(agent->jctx, msg);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
int id = json_get_int(agent, msg, "id", 0);
|
|
1100
|
+
const char *method = json_get_string(agent, msg, "method");
|
|
1101
|
+
JSValue params = json_get(agent, msg, "params");
|
|
1102
|
+
if (!JS_IsObject(params)) {
|
|
1103
|
+
JS_FreeValue(agent->jctx, params);
|
|
1104
|
+
params = json_object(agent);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (method)
|
|
1108
|
+
dispatch(agent, id, method, params);
|
|
1109
|
+
else
|
|
1110
|
+
send_error(agent, id, "Message had no method");
|
|
1111
|
+
|
|
1112
|
+
if (method) JS_FreeCString(agent->jctx, method);
|
|
1113
|
+
JS_FreeValue(agent->jctx, params);
|
|
1114
|
+
JS_FreeValue(agent->jctx, msg);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/* -------------------------------------------------------- the C API ------ */
|
|
1118
|
+
|
|
1119
|
+
QJSCDPAgent *qjs_cdp_new(
|
|
1120
|
+
JSContext *ctx, int execution_context_id, QJSCDPSendFunc *send,
|
|
1121
|
+
void *send_opaque) {
|
|
1122
|
+
QJSCDPAgent *agent = calloc(1, sizeof(*agent));
|
|
1123
|
+
agent->ctx = ctx;
|
|
1124
|
+
agent->exec_ctx_id = execution_context_id;
|
|
1125
|
+
agent->send = send;
|
|
1126
|
+
agent->send_opaque = send_opaque;
|
|
1127
|
+
agent->breakpoints_active = true;
|
|
1128
|
+
agent->mode = MODE_RUN;
|
|
1129
|
+
pthread_mutex_init(&agent->lock, NULL);
|
|
1130
|
+
pthread_cond_init(&agent->cond, NULL);
|
|
1131
|
+
|
|
1132
|
+
/* Base objects and JSON, nothing else: this runtime never runs a program. */
|
|
1133
|
+
agent->jrt = JS_NewRuntime();
|
|
1134
|
+
agent->jctx = JS_NewContextRaw(agent->jrt);
|
|
1135
|
+
JS_AddIntrinsicBaseObjects(agent->jctx);
|
|
1136
|
+
JS_AddIntrinsicJSON(agent->jctx);
|
|
1137
|
+
|
|
1138
|
+
JS_SetDebugTraceHandler(ctx, on_statement, agent);
|
|
1139
|
+
JS_SetDebugTraceArmed(ctx, false);
|
|
1140
|
+
return agent;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
void qjs_cdp_free(QJSCDPAgent *agent) {
|
|
1144
|
+
if (!agent) return;
|
|
1145
|
+
JS_SetDebugTraceHandler(agent->ctx, NULL, NULL);
|
|
1146
|
+
|
|
1147
|
+
for (int i = 0; i < agent->nobjs; i++) {
|
|
1148
|
+
JS_FreeValue(agent->ctx, agent->objs[i].value);
|
|
1149
|
+
free(agent->objs[i].group);
|
|
1150
|
+
}
|
|
1151
|
+
for (int i = 0; i < agent->nscripts; i++) {
|
|
1152
|
+
free(agent->scripts[i].url);
|
|
1153
|
+
free(agent->scripts[i].source);
|
|
1154
|
+
}
|
|
1155
|
+
for (int i = 0; i < agent->nbps; i++) {
|
|
1156
|
+
free(agent->bps[i].url);
|
|
1157
|
+
free(agent->bps[i].condition);
|
|
1158
|
+
}
|
|
1159
|
+
for (int i = 0; i < agent->queue_len; i++) free(agent->queue[i]);
|
|
1160
|
+
|
|
1161
|
+
free(agent->objs);
|
|
1162
|
+
free(agent->scripts);
|
|
1163
|
+
free(agent->bps);
|
|
1164
|
+
free(agent->queue);
|
|
1165
|
+
free(agent->queue_session);
|
|
1166
|
+
|
|
1167
|
+
JS_FreeContext(agent->jctx);
|
|
1168
|
+
JS_FreeRuntime(agent->jrt);
|
|
1169
|
+
pthread_cond_destroy(&agent->cond);
|
|
1170
|
+
pthread_mutex_destroy(&agent->lock);
|
|
1171
|
+
free(agent);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
void qjs_cdp_send_message(
|
|
1175
|
+
QJSCDPAgent *agent, void *session, const char *json, size_t len) {
|
|
1176
|
+
char *copy = malloc(len + 1);
|
|
1177
|
+
memcpy(copy, json, len);
|
|
1178
|
+
copy[len] = '\0';
|
|
1179
|
+
|
|
1180
|
+
pthread_mutex_lock(&agent->lock);
|
|
1181
|
+
if (agent->queue_len == agent->queue_cap) {
|
|
1182
|
+
agent->queue_cap = agent->queue_cap ? agent->queue_cap * 2 : 8;
|
|
1183
|
+
agent->queue =
|
|
1184
|
+
realloc(agent->queue, sizeof(char *) * (size_t)agent->queue_cap);
|
|
1185
|
+
agent->queue_session = realloc(
|
|
1186
|
+
agent->queue_session, sizeof(void *) * (size_t)agent->queue_cap);
|
|
1187
|
+
}
|
|
1188
|
+
agent->queue_session[agent->queue_len] = session;
|
|
1189
|
+
agent->queue[agent->queue_len++] = copy;
|
|
1190
|
+
pthread_cond_broadcast(&agent->cond);
|
|
1191
|
+
pthread_mutex_unlock(&agent->lock);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
void qjs_cdp_poll(QJSCDPAgent *agent) {
|
|
1195
|
+
enter_agent_runtime(agent);
|
|
1196
|
+
for (;;) {
|
|
1197
|
+
void *session = NULL;
|
|
1198
|
+
pthread_mutex_lock(&agent->lock);
|
|
1199
|
+
char *msg = take_message(agent, &session);
|
|
1200
|
+
pthread_mutex_unlock(&agent->lock);
|
|
1201
|
+
if (!msg) return;
|
|
1202
|
+
agent->session = session;
|
|
1203
|
+
handle_message(agent, msg, strlen(msg));
|
|
1204
|
+
agent->session = NULL;
|
|
1205
|
+
free(msg);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
void qjs_cdp_console_message(
|
|
1210
|
+
QJSCDPAgent *agent, const char *type, const JSValue *args, int count) {
|
|
1211
|
+
enter_agent_runtime(agent);
|
|
1212
|
+
JSValue list = json_array(agent);
|
|
1213
|
+
for (int i = 0; i < count; i++)
|
|
1214
|
+
json_set_index(
|
|
1215
|
+
agent, list, (uint32_t)i, to_remote_object(agent, args[i], NULL));
|
|
1216
|
+
|
|
1217
|
+
JSValue params = json_object(agent);
|
|
1218
|
+
json_set_string(agent, params, "type", type ? type : "log");
|
|
1219
|
+
json_set(agent, params, "args", list);
|
|
1220
|
+
json_set_int(agent, params, "executionContextId", agent->exec_ctx_id);
|
|
1221
|
+
json_set(agent, params, "timestamp", JS_NewFloat64(agent->jctx, 0));
|
|
1222
|
+
send_event(agent, "Runtime.consoleAPICalled", params);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
void qjs_cdp_script_loaded(
|
|
1226
|
+
QJSCDPAgent *agent, const char *url, const char *source) {
|
|
1227
|
+
enter_agent_runtime(agent);
|
|
1228
|
+
agent->scripts =
|
|
1229
|
+
realloc(agent->scripts, sizeof(Script) * (size_t)(agent->nscripts + 1));
|
|
1230
|
+
Script *script = &agent->scripts[agent->nscripts++];
|
|
1231
|
+
script->id = agent->nscripts;
|
|
1232
|
+
script->url = strdup(url ? url : "");
|
|
1233
|
+
script->source = strdup(source ? source : "");
|
|
1234
|
+
|
|
1235
|
+
if (agent->debugger_enabled) {
|
|
1236
|
+
announce_script(agent, script, true);
|
|
1237
|
+
resolve_breakpoints(agent, script);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
char *qjs_cdp_export_state(QJSCDPAgent *agent) {
|
|
1242
|
+
enter_agent_runtime(agent);
|
|
1243
|
+
JSValue list = json_array(agent);
|
|
1244
|
+
for (int i = 0; i < agent->nbps; i++) {
|
|
1245
|
+
Breakpoint *bp = &agent->bps[i];
|
|
1246
|
+
JSValue e = json_object(agent);
|
|
1247
|
+
json_set_int(agent, e, "id", bp->id);
|
|
1248
|
+
json_set_string(agent, e, "url", bp->url);
|
|
1249
|
+
json_set_int(agent, e, "line", bp->line);
|
|
1250
|
+
json_set_int(agent, e, "col", bp->col);
|
|
1251
|
+
if (bp->condition) json_set_string(agent, e, "condition", bp->condition);
|
|
1252
|
+
json_set_index(agent, list, (uint32_t)i, e);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
JSValue state = json_object(agent);
|
|
1256
|
+
json_set_bool(agent, state, "enabled", agent->debugger_enabled);
|
|
1257
|
+
json_set_int(agent, state, "nextBreakpointId", agent->next_bp_id);
|
|
1258
|
+
json_set(agent, state, "breakpoints", list);
|
|
1259
|
+
|
|
1260
|
+
JSValue text =
|
|
1261
|
+
JS_JSONStringify(agent->jctx, state, JS_UNDEFINED, JS_UNDEFINED);
|
|
1262
|
+
size_t len = 0;
|
|
1263
|
+
const char *json = JS_ToCStringLen(agent->jctx, &len, text);
|
|
1264
|
+
char *out = NULL;
|
|
1265
|
+
if (json) {
|
|
1266
|
+
out = malloc(len + 1);
|
|
1267
|
+
memcpy(out, json, len);
|
|
1268
|
+
out[len] = '\0';
|
|
1269
|
+
JS_FreeCString(agent->jctx, json);
|
|
1270
|
+
}
|
|
1271
|
+
JS_FreeValue(agent->jctx, text);
|
|
1272
|
+
JS_FreeValue(agent->jctx, state);
|
|
1273
|
+
return out;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
void qjs_cdp_import_state(QJSCDPAgent *agent, const char *json) {
|
|
1277
|
+
enter_agent_runtime(agent);
|
|
1278
|
+
if (!json) return;
|
|
1279
|
+
JSValue state = JS_ParseJSON(agent->jctx, json, strlen(json), "<state>");
|
|
1280
|
+
if (JS_IsException(state)) {
|
|
1281
|
+
JS_FreeValue(agent->jctx, JS_GetException(agent->jctx));
|
|
1282
|
+
JS_FreeValue(agent->jctx, state);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
agent->debugger_enabled = json_get_bool(agent, state, "enabled", false);
|
|
1287
|
+
if (agent->debugger_enabled) JS_SetDebugTraceArmed(agent->ctx, true);
|
|
1288
|
+
agent->next_bp_id = json_get_int(agent, state, "nextBreakpointId", 0);
|
|
1289
|
+
|
|
1290
|
+
JSValue list = json_get(agent, state, "breakpoints");
|
|
1291
|
+
const int count = json_get_int(agent, list, "length", 0);
|
|
1292
|
+
for (int i = 0; i < count; i++) {
|
|
1293
|
+
JSValue e = JS_GetPropertyUint32(agent->jctx, list, (uint32_t)i);
|
|
1294
|
+
const char *url = json_get_string(agent, e, "url");
|
|
1295
|
+
if (url) {
|
|
1296
|
+
const char *condition = json_get_string(agent, e, "condition");
|
|
1297
|
+
agent->bps = realloc(agent->bps, sizeof(Breakpoint) * (agent->nbps + 1));
|
|
1298
|
+
Breakpoint *bp = &agent->bps[agent->nbps++];
|
|
1299
|
+
bp->id = json_get_int(agent, e, "id", 0);
|
|
1300
|
+
bp->url = strdup(url);
|
|
1301
|
+
bp->condition = condition ? strdup(condition) : NULL;
|
|
1302
|
+
bp->line = json_get_int(agent, e, "line", 1);
|
|
1303
|
+
bp->col = json_get_int(agent, e, "col", 1);
|
|
1304
|
+
/* Unresolved again on purpose: the scripts are gone with the old runtime,
|
|
1305
|
+
and each one that comes back re-announces the breakpoints it binds. */
|
|
1306
|
+
bp->resolved = false;
|
|
1307
|
+
if (condition) JS_FreeCString(agent->jctx, condition);
|
|
1308
|
+
JS_FreeCString(agent->jctx, url);
|
|
1309
|
+
}
|
|
1310
|
+
JS_FreeValue(agent->jctx, e);
|
|
1311
|
+
}
|
|
1312
|
+
JS_FreeValue(agent->jctx, list);
|
|
1313
|
+
JS_FreeValue(agent->jctx, state);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
bool qjs_cdp_debugger_enabled(QJSCDPAgent *agent) {
|
|
1317
|
+
return agent->debugger_enabled;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
void qjs_cdp_set_debugger_enabled(QJSCDPAgent *agent, bool enabled) {
|
|
1321
|
+
agent->debugger_enabled = enabled;
|
|
1322
|
+
/* Disarmed rather than uninstalled: uninstalling the handler would
|
|
1323
|
+
permanently un-instrument everything parsed afterwards. */
|
|
1324
|
+
JS_SetDebugTraceArmed(agent->ctx, enabled);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
char *qjs_cdp_capture_stack_trace(QJSCDPAgent *agent, int frames_to_skip) {
|
|
1328
|
+
enter_agent_runtime(agent);
|
|
1329
|
+
JSValue frames = json_array(agent);
|
|
1330
|
+
const int depth = JS_GetStackDepth(agent->ctx);
|
|
1331
|
+
uint32_t n = 0;
|
|
1332
|
+
|
|
1333
|
+
for (int level = frames_to_skip; level < depth; level++) {
|
|
1334
|
+
JSDebugFrameInfo info;
|
|
1335
|
+
if (JS_GetFrameInfoAtLevel(agent->ctx, level, &info) < 0) break;
|
|
1336
|
+
if (info.is_native) continue;
|
|
1337
|
+
|
|
1338
|
+
const char *file = info.filename != JS_ATOM_NULL
|
|
1339
|
+
? JS_AtomToCString(agent->ctx, info.filename)
|
|
1340
|
+
: NULL;
|
|
1341
|
+
const char *func = info.func_name != JS_ATOM_NULL
|
|
1342
|
+
? JS_AtomToCString(agent->ctx, info.func_name)
|
|
1343
|
+
: NULL;
|
|
1344
|
+
Script *script = file ? find_script_by_url(agent, file) : NULL;
|
|
1345
|
+
char script_id[16];
|
|
1346
|
+
snprintf(script_id, sizeof(script_id), "%d", script ? script->id : 0);
|
|
1347
|
+
|
|
1348
|
+
JSValue frame = json_object(agent);
|
|
1349
|
+
json_set_string(agent, frame, "functionName", frame_name(func));
|
|
1350
|
+
json_set_string(agent, frame, "scriptId", script_id);
|
|
1351
|
+
json_set_string(agent, frame, "url", file ? file : "");
|
|
1352
|
+
json_set_int(agent, frame, "lineNumber", info.line > 0 ? info.line - 1 : 0);
|
|
1353
|
+
json_set_int(agent, frame, "columnNumber", info.col > 0 ? info.col - 1 : 0);
|
|
1354
|
+
json_set_index(agent, frames, n++, frame);
|
|
1355
|
+
|
|
1356
|
+
if (file) JS_FreeCString(agent->ctx, file);
|
|
1357
|
+
if (func) JS_FreeCString(agent->ctx, func);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
JSValue trace = json_object(agent);
|
|
1361
|
+
json_set(agent, trace, "callFrames", frames);
|
|
1362
|
+
|
|
1363
|
+
JSValue text =
|
|
1364
|
+
JS_JSONStringify(agent->jctx, trace, JS_UNDEFINED, JS_UNDEFINED);
|
|
1365
|
+
size_t len = 0;
|
|
1366
|
+
const char *json = JS_ToCStringLen(agent->jctx, &len, text);
|
|
1367
|
+
char *out = NULL;
|
|
1368
|
+
if (json) {
|
|
1369
|
+
out = malloc(len + 1);
|
|
1370
|
+
memcpy(out, json, len);
|
|
1371
|
+
out[len] = '\0';
|
|
1372
|
+
JS_FreeCString(agent->jctx, json);
|
|
1373
|
+
}
|
|
1374
|
+
JS_FreeValue(agent->jctx, text);
|
|
1375
|
+
JS_FreeValue(agent->jctx, trace);
|
|
1376
|
+
return out;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
void qjs_cdp_set_execution_context(QJSCDPAgent *agent, int id) {
|
|
1380
|
+
agent->exec_ctx_id = id;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
void qjs_cdp_pause(QJSCDPAgent *agent) {
|
|
1384
|
+
enter_agent_runtime(agent);
|
|
1385
|
+
agent->pause_pending = true;
|
|
1386
|
+
}
|