dsh-ris-ide 0.0.0-stage → 1.6.9
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/LICENSE +21 -0
- package/README.md +124 -2
- package/bin/run-with-log.ps1 +173 -0
- package/bin/run-with-log.sh +136 -0
- package/cordis.patch.yml +25 -0
- package/lib/client-codemirror.js +29822 -0
- package/lib/client.js +10772 -0
- package/lib/debug/engine.js +537 -0
- package/lib/debug/vendor/LICENSE +21 -0
- package/lib/debug/vendor/README.md +36 -0
- package/lib/debug/vendor/UPSTREAM_COMMIT +1 -0
- package/lib/debug/vendor/jdwp/client.js +1104 -0
- package/lib/debug/vendor/jdwp/constants.js +309 -0
- package/lib/debug/vendor/jdwp/protocol.js +253 -0
- package/lib/debug/vendor/src/jdwp/client.ts +1368 -0
- package/lib/debug/vendor/src/jdwp/constants.ts +310 -0
- package/lib/debug/vendor/src/jdwp/protocol.ts +326 -0
- package/lib/index.js +6112 -0
- package/lib/standalone/vendor/react-dom.js +267 -0
- package/lib/standalone/vendor/react.js +31 -0
- package/lib/standalone/window.html +248 -0
- package/package.json +96 -4
- package/skills/dsh-ris-ide-debug/SKILL.md +158 -0
- package/test/client-viewer.test.mjs +3557 -0
- package/test/debug-engine.test.mjs +351 -0
- package/test/harness.test.mjs +93 -0
- package/test/host-nogit.test.mjs +48 -0
- package/test/host-tail.test.mjs +2846 -0
- package/test/host-window.test.mjs +119 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { HANDSHAKE, FLAGS_REPLY, CommandSet, VMCommand, ReferenceTypeCommand, MethodCommand, ThreadCommand, StackFrameCommand, EventRequestCommand, StringReferenceCommand, ObjectReferenceCommand, ArrayReferenceCommand, EventKind, SuspendPolicy, StepSize, StepDepth, ModKind, TypeTag, Tag, } from "./constants.js";
|
|
4
|
+
import { JDWPWriter, JDWPReader, buildCommandPacket, hasCompletePacket, getPacketLength, isReplyPacket, } from "./protocol.js";
|
|
5
|
+
/**
|
|
6
|
+
* Event kinds that should NOT trigger an automatic VM resume.
|
|
7
|
+
* When the JVM sends these events with a suspend policy, the VM stays suspended
|
|
8
|
+
* so the user (or AI assistant) can inspect state and set breakpoints before resuming.
|
|
9
|
+
*/
|
|
10
|
+
const SUSPEND_PRESERVING_EVENTS = new Set([
|
|
11
|
+
EventKind.Breakpoint,
|
|
12
|
+
EventKind.SingleStep,
|
|
13
|
+
EventKind.VMStart,
|
|
14
|
+
EventKind.ClassPrepare, // Keep VM suspended so breakpoints can be set before execution continues
|
|
15
|
+
]);
|
|
16
|
+
export class JDWPClient extends EventEmitter {
|
|
17
|
+
socket = null;
|
|
18
|
+
connected = false;
|
|
19
|
+
handshakeComplete = false;
|
|
20
|
+
nextId = 1;
|
|
21
|
+
pendingRequests = new Map();
|
|
22
|
+
receiveBuffer = Buffer.alloc(0);
|
|
23
|
+
idSizes = {
|
|
24
|
+
fieldIDSize: 8,
|
|
25
|
+
methodIDSize: 8,
|
|
26
|
+
objectIDSize: 8,
|
|
27
|
+
referenceTypeIDSize: 8,
|
|
28
|
+
frameIDSize: 8,
|
|
29
|
+
};
|
|
30
|
+
breakpoints = new Map();
|
|
31
|
+
classPrepareRequests = new Map();
|
|
32
|
+
// Consolidated thread/VM suspend state
|
|
33
|
+
threadState = {
|
|
34
|
+
/** Threads currently suspended by breakpoints/steps */
|
|
35
|
+
suspended: new Set(),
|
|
36
|
+
/** Most recently suspended thread (convenience default) */
|
|
37
|
+
lastSuspendedId: null,
|
|
38
|
+
/** Whether the entire VM is suspended (e.g. from suspend=y launch) */
|
|
39
|
+
vmSuspended: false,
|
|
40
|
+
};
|
|
41
|
+
get isConnected() {
|
|
42
|
+
return this.connected && this.handshakeComplete;
|
|
43
|
+
}
|
|
44
|
+
/** Whether the entire VM is currently suspended (e.g. from suspend=y launch) */
|
|
45
|
+
get vmSuspended() {
|
|
46
|
+
return this.threadState.vmSuspended;
|
|
47
|
+
}
|
|
48
|
+
/** The most recently suspended thread (from breakpoint or step) */
|
|
49
|
+
get currentThreadId() {
|
|
50
|
+
return this.threadState.lastSuspendedId;
|
|
51
|
+
}
|
|
52
|
+
/** All threads currently known to be suspended by breakpoints/steps */
|
|
53
|
+
get allSuspendedThreadIds() {
|
|
54
|
+
return Array.from(this.threadState.suspended);
|
|
55
|
+
}
|
|
56
|
+
// --- Thread state mutation helpers ---
|
|
57
|
+
markThreadSuspended(threadId) {
|
|
58
|
+
this.threadState.suspended.add(threadId);
|
|
59
|
+
this.threadState.lastSuspendedId = threadId;
|
|
60
|
+
}
|
|
61
|
+
markThreadResumed(threadId) {
|
|
62
|
+
this.threadState.suspended.delete(threadId);
|
|
63
|
+
if (this.threadState.lastSuspendedId === threadId) {
|
|
64
|
+
const remaining = this.allSuspendedThreadIds;
|
|
65
|
+
this.threadState.lastSuspendedId =
|
|
66
|
+
remaining.length > 0 ? remaining[remaining.length - 1] : null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
markAllResumed() {
|
|
70
|
+
this.threadState.suspended.clear();
|
|
71
|
+
this.threadState.lastSuspendedId = null;
|
|
72
|
+
this.threadState.vmSuspended = false;
|
|
73
|
+
}
|
|
74
|
+
resetThreadState() {
|
|
75
|
+
this.threadState.suspended.clear();
|
|
76
|
+
this.threadState.lastSuspendedId = null;
|
|
77
|
+
this.threadState.vmSuspended = false;
|
|
78
|
+
}
|
|
79
|
+
async connect(host, port) {
|
|
80
|
+
return await new Promise((resolve, reject) => {
|
|
81
|
+
this.socket = net.createConnection({ host, port, autoSelectFamily: true }, () => {
|
|
82
|
+
// Disable Nagle's algorithm for low-latency JDWP communication
|
|
83
|
+
this.socket?.setNoDelay(true);
|
|
84
|
+
this.socket?.setKeepAlive(true, 30000);
|
|
85
|
+
// Send handshake
|
|
86
|
+
this.socket?.write(HANDSHAKE);
|
|
87
|
+
});
|
|
88
|
+
let handshakeBuffer = Buffer.alloc(0);
|
|
89
|
+
const onData = (data) => {
|
|
90
|
+
if (!this.handshakeComplete) {
|
|
91
|
+
handshakeBuffer = Buffer.concat([handshakeBuffer, data]);
|
|
92
|
+
if (handshakeBuffer.length >= HANDSHAKE.length) {
|
|
93
|
+
const response = handshakeBuffer.subarray(0, HANDSHAKE.length).toString();
|
|
94
|
+
if (response === HANDSHAKE) {
|
|
95
|
+
this.handshakeComplete = true;
|
|
96
|
+
this.connected = true;
|
|
97
|
+
// Process any remaining data after handshake
|
|
98
|
+
this.receiveBuffer = handshakeBuffer.subarray(HANDSHAKE.length);
|
|
99
|
+
this.socket?.removeListener("data", onData);
|
|
100
|
+
this.socket?.on("data", (d) => this.onData(d));
|
|
101
|
+
this.processBuffer();
|
|
102
|
+
// Get ID sizes and version
|
|
103
|
+
this.initialize().then((version) => resolve(version), (err) => reject(err instanceof Error ? err : new Error(String(err))));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
reject(new Error("Invalid JDWP handshake response"));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
this.socket.on("data", onData);
|
|
112
|
+
this.socket.on("error", (err) => {
|
|
113
|
+
this.connected = false;
|
|
114
|
+
if (!this.handshakeComplete) {
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
117
|
+
this.emit("error", err);
|
|
118
|
+
});
|
|
119
|
+
this.socket.on("close", () => {
|
|
120
|
+
this.connected = false;
|
|
121
|
+
this.handshakeComplete = false;
|
|
122
|
+
this.emit("close");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async disconnect() {
|
|
127
|
+
if (this.socket) {
|
|
128
|
+
// Send dispose command
|
|
129
|
+
try {
|
|
130
|
+
await this.sendCommand(CommandSet.VirtualMachine, VMCommand.Dispose);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Ignore errors during disconnect
|
|
134
|
+
}
|
|
135
|
+
this.socket.destroy();
|
|
136
|
+
this.socket = null;
|
|
137
|
+
this.connected = false;
|
|
138
|
+
this.handshakeComplete = false;
|
|
139
|
+
this.breakpoints.clear();
|
|
140
|
+
this.resetThreadState();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async initialize() {
|
|
144
|
+
// Get ID sizes first - critical for all subsequent communication
|
|
145
|
+
const idReply = await this.sendCommand(CommandSet.VirtualMachine, VMCommand.IDSizes);
|
|
146
|
+
const idReader = new JDWPReader(idReply.data);
|
|
147
|
+
this.idSizes = {
|
|
148
|
+
fieldIDSize: idReader.readInt(),
|
|
149
|
+
methodIDSize: idReader.readInt(),
|
|
150
|
+
objectIDSize: idReader.readInt(),
|
|
151
|
+
referenceTypeIDSize: idReader.readInt(),
|
|
152
|
+
frameIDSize: idReader.readInt(),
|
|
153
|
+
};
|
|
154
|
+
// Get version info
|
|
155
|
+
const verReply = await this.sendCommand(CommandSet.VirtualMachine, VMCommand.Version);
|
|
156
|
+
const verReader = new JDWPReader(verReply.data, this.idSizes);
|
|
157
|
+
verReader.readString(); // description
|
|
158
|
+
verReader.readInt(); // jdwpMajor
|
|
159
|
+
verReader.readInt(); // jdwpMinor
|
|
160
|
+
const vmVersion = verReader.readString();
|
|
161
|
+
const vmName = verReader.readString();
|
|
162
|
+
return `${vmName} ${vmVersion}`;
|
|
163
|
+
}
|
|
164
|
+
onData(data) {
|
|
165
|
+
this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]);
|
|
166
|
+
this.processBuffer();
|
|
167
|
+
}
|
|
168
|
+
processBuffer() {
|
|
169
|
+
while (hasCompletePacket(this.receiveBuffer)) {
|
|
170
|
+
const length = getPacketLength(this.receiveBuffer);
|
|
171
|
+
const packet = this.receiveBuffer.subarray(0, length);
|
|
172
|
+
this.receiveBuffer = this.receiveBuffer.subarray(length);
|
|
173
|
+
if (isReplyPacket(packet)) {
|
|
174
|
+
// Reply packet
|
|
175
|
+
const id = packet.readInt32BE(4);
|
|
176
|
+
const errorCode = packet.readUInt16BE(9);
|
|
177
|
+
const data = packet.subarray(11);
|
|
178
|
+
const pending = this.pendingRequests.get(id);
|
|
179
|
+
if (pending) {
|
|
180
|
+
this.pendingRequests.delete(id);
|
|
181
|
+
pending.resolve({ id, flags: FLAGS_REPLY, errorCode, data });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Event/command packet from VM
|
|
186
|
+
this.handleEvent(packet);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
handleEvent(packet) {
|
|
191
|
+
const commandSet = packet.readUInt8(9);
|
|
192
|
+
const command = packet.readUInt8(10);
|
|
193
|
+
const data = packet.subarray(11);
|
|
194
|
+
if (commandSet === CommandSet.Event && command === 100) {
|
|
195
|
+
// Composite event
|
|
196
|
+
const reader = new JDWPReader(data, this.idSizes);
|
|
197
|
+
const suspendPolicy = reader.readByte();
|
|
198
|
+
const eventCount = reader.readInt();
|
|
199
|
+
const observedEventKinds = [];
|
|
200
|
+
for (let i = 0; i < eventCount; i++) {
|
|
201
|
+
const eventKind = reader.readByte();
|
|
202
|
+
observedEventKinds.push(eventKind);
|
|
203
|
+
switch (eventKind) {
|
|
204
|
+
case EventKind.VMStart: {
|
|
205
|
+
const requestId = reader.readInt();
|
|
206
|
+
const threadId = reader.readThreadID();
|
|
207
|
+
this.emit("vmstart", { requestId, threadId });
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case EventKind.VMDeath: {
|
|
211
|
+
const requestId = reader.readInt();
|
|
212
|
+
this.emit("vmdeath", { requestId });
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case EventKind.Breakpoint: {
|
|
216
|
+
const requestId = reader.readInt();
|
|
217
|
+
const threadId = reader.readThreadID();
|
|
218
|
+
const location = reader.readLocation();
|
|
219
|
+
this.markThreadSuspended(threadId);
|
|
220
|
+
this.emit("breakpoint", { requestId, threadId, location });
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case EventKind.SingleStep: {
|
|
224
|
+
const requestId = reader.readInt();
|
|
225
|
+
const threadId = reader.readThreadID();
|
|
226
|
+
const location = reader.readLocation();
|
|
227
|
+
this.markThreadSuspended(threadId);
|
|
228
|
+
// Auto-clear single step request
|
|
229
|
+
this.clearEventRequest(EventKind.SingleStep, requestId).catch(() => { });
|
|
230
|
+
this.emit("step", { requestId, threadId, location });
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case EventKind.ClassPrepare: {
|
|
234
|
+
const requestId = reader.readInt();
|
|
235
|
+
const threadId = reader.readThreadID();
|
|
236
|
+
const refTypeTag = reader.readByte();
|
|
237
|
+
const typeID = reader.readReferenceTypeID();
|
|
238
|
+
const signature = reader.readString();
|
|
239
|
+
const status = reader.readInt();
|
|
240
|
+
this.emit("classprepare", {
|
|
241
|
+
requestId,
|
|
242
|
+
threadId,
|
|
243
|
+
refTypeTag,
|
|
244
|
+
typeID,
|
|
245
|
+
signature,
|
|
246
|
+
status,
|
|
247
|
+
});
|
|
248
|
+
// Check if we're waiting for this class
|
|
249
|
+
const className = jniSignatureToClassName(signature);
|
|
250
|
+
const waiting = this.classPrepareRequests.get(className);
|
|
251
|
+
if (waiting) {
|
|
252
|
+
this.classPrepareRequests.delete(className);
|
|
253
|
+
// Clear the class prepare request
|
|
254
|
+
this.clearEventRequest(EventKind.ClassPrepare, waiting.requestId).catch(() => { });
|
|
255
|
+
waiting.resolve(typeID);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case EventKind.ThreadStart: {
|
|
260
|
+
const requestId = reader.readInt();
|
|
261
|
+
const threadId = reader.readThreadID();
|
|
262
|
+
this.emit("threadstart", { requestId, threadId });
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case EventKind.ThreadDeath: {
|
|
266
|
+
const requestId = reader.readInt();
|
|
267
|
+
const threadId = reader.readThreadID();
|
|
268
|
+
this.emit("threaddeath", { requestId, threadId });
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
default: {
|
|
272
|
+
// Skip unknown events - try to read requestId at minimum
|
|
273
|
+
try {
|
|
274
|
+
reader.readInt();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// ignore
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Auto-resume logic: if the VM was suspended by this event, decide whether to keep it
|
|
284
|
+
// suspended or auto-resume. Events in SUSPEND_PRESERVING_EVENTS (breakpoints, steps,
|
|
285
|
+
// VMStart) keep the VM suspended so the user can inspect state / set breakpoints.
|
|
286
|
+
// Internal events (ClassPrepare, ThreadStart, etc.) are auto-resumed.
|
|
287
|
+
if (suspendPolicy === SuspendPolicy.All || suspendPolicy === SuspendPolicy.EventThread) {
|
|
288
|
+
const shouldPreserveSuspend = observedEventKinds.some((kind) => SUSPEND_PRESERVING_EVENTS.has(kind));
|
|
289
|
+
if (shouldPreserveSuspend) {
|
|
290
|
+
// Only mark VM-wide suspend when SuspendPolicy.All is used (e.g. VMStart with suspend=y).
|
|
291
|
+
// SuspendPolicy.EventThread (default for breakpoints/steps) only suspends one thread,
|
|
292
|
+
// so the VM as a whole is not suspended.
|
|
293
|
+
if (suspendPolicy === SuspendPolicy.All) {
|
|
294
|
+
this.threadState.vmSuspended = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.resumeVM().catch(() => { });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
sendCommand(commandSet, command, data) {
|
|
304
|
+
return new Promise((resolve, reject) => {
|
|
305
|
+
if (!this.socket || !this.connected) {
|
|
306
|
+
reject(new Error("Not connected to JVM"));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const id = this.nextId++;
|
|
310
|
+
const packet = buildCommandPacket(id, commandSet, command, data);
|
|
311
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
312
|
+
this.socket.write(packet);
|
|
313
|
+
// Timeout after 10 seconds
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
if (this.pendingRequests.has(id)) {
|
|
316
|
+
this.pendingRequests.delete(id);
|
|
317
|
+
reject(new Error(`Command timeout: cmdSet=${commandSet}, cmd=${command}`));
|
|
318
|
+
}
|
|
319
|
+
}, 10000);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
checkError(reply) {
|
|
323
|
+
if (reply.errorCode !== 0) {
|
|
324
|
+
throw new Error(`JDWP error: ${reply.errorCode}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* High-level helper: build request data, send command, check error, parse reply.
|
|
329
|
+
* Eliminates the repeated Writer→sendCommand→checkError→Reader boilerplate.
|
|
330
|
+
*/
|
|
331
|
+
async sendRequest(commandSet, command, writeData, readReply) {
|
|
332
|
+
let data;
|
|
333
|
+
if (writeData) {
|
|
334
|
+
const writer = new JDWPWriter(this.idSizes);
|
|
335
|
+
writeData(writer);
|
|
336
|
+
data = writer.toBuffer();
|
|
337
|
+
}
|
|
338
|
+
const reply = await this.sendCommand(commandSet, command, data);
|
|
339
|
+
this.checkError(reply);
|
|
340
|
+
const reader = new JDWPReader(reply.data, this.idSizes);
|
|
341
|
+
return readReply(reader);
|
|
342
|
+
}
|
|
343
|
+
// === High-level API ===
|
|
344
|
+
async getVersion() {
|
|
345
|
+
return this.sendRequest(CommandSet.VirtualMachine, VMCommand.Version, null, (r) => {
|
|
346
|
+
r.readString(); // description
|
|
347
|
+
r.readInt(); // jdwpMajor
|
|
348
|
+
r.readInt(); // jdwpMinor
|
|
349
|
+
const vmVersion = r.readString();
|
|
350
|
+
const vmName = r.readString();
|
|
351
|
+
return `${vmName} ${vmVersion}`;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async suspendVM() {
|
|
355
|
+
const reply = await this.sendCommand(CommandSet.VirtualMachine, VMCommand.Suspend);
|
|
356
|
+
this.checkError(reply);
|
|
357
|
+
this.threadState.vmSuspended = true;
|
|
358
|
+
}
|
|
359
|
+
async resumeVM() {
|
|
360
|
+
const reply = await this.sendCommand(CommandSet.VirtualMachine, VMCommand.Resume);
|
|
361
|
+
this.checkError(reply);
|
|
362
|
+
this.markAllResumed();
|
|
363
|
+
}
|
|
364
|
+
async getClassesBySignature(signature) {
|
|
365
|
+
return this.sendRequest(CommandSet.VirtualMachine, VMCommand.ClassesBySignature, (w) => w.writeString(signature), (r) => {
|
|
366
|
+
const count = r.readInt();
|
|
367
|
+
const classes = [];
|
|
368
|
+
for (let i = 0; i < count; i++) {
|
|
369
|
+
classes.push({
|
|
370
|
+
refTypeTag: r.readByte(),
|
|
371
|
+
typeID: r.readReferenceTypeID(),
|
|
372
|
+
status: r.readInt(),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return classes;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async getReferenceTypeSignature(refTypeId) {
|
|
379
|
+
return this.sendRequest(CommandSet.ReferenceType, ReferenceTypeCommand.Signature, (w) => w.writeReferenceTypeID(refTypeId), (r) => r.readString());
|
|
380
|
+
}
|
|
381
|
+
async getSourceFile(refTypeId) {
|
|
382
|
+
return this.sendRequest(CommandSet.ReferenceType, ReferenceTypeCommand.SourceFile, (w) => w.writeReferenceTypeID(refTypeId), (r) => r.readString());
|
|
383
|
+
}
|
|
384
|
+
async getMethods(refTypeId) {
|
|
385
|
+
return this.sendRequest(CommandSet.ReferenceType, ReferenceTypeCommand.Methods, (w) => w.writeReferenceTypeID(refTypeId), (r) => {
|
|
386
|
+
const count = r.readInt();
|
|
387
|
+
const methods = [];
|
|
388
|
+
for (let i = 0; i < count; i++) {
|
|
389
|
+
methods.push({
|
|
390
|
+
methodID: r.readMethodID(),
|
|
391
|
+
name: r.readString(),
|
|
392
|
+
signature: r.readString(),
|
|
393
|
+
modBits: r.readInt(),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return methods;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
async getFields(refTypeId) {
|
|
400
|
+
return this.sendRequest(CommandSet.ReferenceType, ReferenceTypeCommand.Fields, (w) => w.writeReferenceTypeID(refTypeId), (r) => {
|
|
401
|
+
const count = r.readInt();
|
|
402
|
+
const fields = [];
|
|
403
|
+
for (let i = 0; i < count; i++) {
|
|
404
|
+
fields.push({
|
|
405
|
+
fieldID: r.readFieldID(),
|
|
406
|
+
name: r.readString(),
|
|
407
|
+
signature: r.readString(),
|
|
408
|
+
modBits: r.readInt(),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return fields;
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
async getLineTable(refTypeId, methodId) {
|
|
415
|
+
return this.sendRequest(CommandSet.Method, MethodCommand.LineTable, (w) => {
|
|
416
|
+
w.writeReferenceTypeID(refTypeId);
|
|
417
|
+
w.writeMethodID(methodId);
|
|
418
|
+
}, (r) => {
|
|
419
|
+
const start = r.readLong();
|
|
420
|
+
const end = r.readLong();
|
|
421
|
+
const lineCount = r.readInt();
|
|
422
|
+
const lines = [];
|
|
423
|
+
for (let i = 0; i < lineCount; i++) {
|
|
424
|
+
lines.push({
|
|
425
|
+
lineCodeIndex: r.readLong(),
|
|
426
|
+
lineNumber: r.readInt(),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return { start, end, lines };
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async getVariableTable(refTypeId, methodId) {
|
|
433
|
+
return this.sendRequest(CommandSet.Method, MethodCommand.VariableTable, (w) => {
|
|
434
|
+
w.writeReferenceTypeID(refTypeId);
|
|
435
|
+
w.writeMethodID(methodId);
|
|
436
|
+
}, (r) => {
|
|
437
|
+
r.readInt(); // argCnt
|
|
438
|
+
const count = r.readInt();
|
|
439
|
+
const variables = [];
|
|
440
|
+
for (let i = 0; i < count; i++) {
|
|
441
|
+
variables.push({
|
|
442
|
+
codeIndex: r.readLong(),
|
|
443
|
+
name: r.readString(),
|
|
444
|
+
signature: r.readString(),
|
|
445
|
+
length: r.readInt(),
|
|
446
|
+
slot: r.readInt(),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return variables;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
// === Thread operations ===
|
|
453
|
+
async getAllThreads() {
|
|
454
|
+
const threadIds = await this.sendRequest(CommandSet.VirtualMachine, VMCommand.AllThreads, null, (r) => {
|
|
455
|
+
const count = r.readInt();
|
|
456
|
+
const ids = [];
|
|
457
|
+
for (let i = 0; i < count; i++)
|
|
458
|
+
ids.push(r.readThreadID());
|
|
459
|
+
return ids;
|
|
460
|
+
});
|
|
461
|
+
const threads = [];
|
|
462
|
+
for (const id of threadIds) {
|
|
463
|
+
try {
|
|
464
|
+
const name = await this.getThreadName(id);
|
|
465
|
+
const statusInfo = await this.getThreadStatus(id);
|
|
466
|
+
threads.push({
|
|
467
|
+
id,
|
|
468
|
+
name,
|
|
469
|
+
status: statusInfo.threadStatus,
|
|
470
|
+
suspendStatus: statusInfo.suspendStatus,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// Thread may have died
|
|
475
|
+
threads.push({ id, name: "<unknown>", status: 0, suspendStatus: 0 });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return threads;
|
|
479
|
+
}
|
|
480
|
+
async getThreadName(threadId) {
|
|
481
|
+
return this.sendRequest(CommandSet.ThreadReference, ThreadCommand.Name, (w) => w.writeThreadID(threadId), (r) => r.readString());
|
|
482
|
+
}
|
|
483
|
+
async getThreadStatus(threadId) {
|
|
484
|
+
return this.sendRequest(CommandSet.ThreadReference, ThreadCommand.Status, (w) => w.writeThreadID(threadId), (r) => ({
|
|
485
|
+
threadStatus: r.readInt(),
|
|
486
|
+
suspendStatus: r.readInt(),
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
async suspendThread(threadId) {
|
|
490
|
+
const writer = new JDWPWriter(this.idSizes);
|
|
491
|
+
writer.writeThreadID(threadId);
|
|
492
|
+
const reply = await this.sendCommand(CommandSet.ThreadReference, ThreadCommand.Suspend, writer.toBuffer());
|
|
493
|
+
this.checkError(reply);
|
|
494
|
+
this.markThreadSuspended(threadId);
|
|
495
|
+
}
|
|
496
|
+
async resumeThread(threadId) {
|
|
497
|
+
const writer = new JDWPWriter(this.idSizes);
|
|
498
|
+
writer.writeThreadID(threadId);
|
|
499
|
+
const reply = await this.sendCommand(CommandSet.ThreadReference, ThreadCommand.Resume, writer.toBuffer());
|
|
500
|
+
this.checkError(reply);
|
|
501
|
+
this.markThreadResumed(threadId);
|
|
502
|
+
}
|
|
503
|
+
// === Stack frames ===
|
|
504
|
+
async getFrames(threadId, startFrame = 0, maxFrames = -1) {
|
|
505
|
+
// Always get actual frame count to avoid INVALID_LENGTH errors
|
|
506
|
+
const countWriter = new JDWPWriter(this.idSizes);
|
|
507
|
+
countWriter.writeThreadID(threadId);
|
|
508
|
+
const countReply = await this.sendCommand(CommandSet.ThreadReference, ThreadCommand.FrameCount, countWriter.toBuffer());
|
|
509
|
+
this.checkError(countReply);
|
|
510
|
+
const countReader = new JDWPReader(countReply.data, this.idSizes);
|
|
511
|
+
const totalFrames = countReader.readInt();
|
|
512
|
+
// Clamp requested frame count to actual available frames
|
|
513
|
+
let length;
|
|
514
|
+
if (maxFrames === -1) {
|
|
515
|
+
length = totalFrames - startFrame;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
length = Math.min(maxFrames, totalFrames - startFrame);
|
|
519
|
+
}
|
|
520
|
+
if (length <= 0)
|
|
521
|
+
return [];
|
|
522
|
+
const writer = new JDWPWriter(this.idSizes);
|
|
523
|
+
writer.writeThreadID(threadId);
|
|
524
|
+
writer.writeInt(startFrame);
|
|
525
|
+
writer.writeInt(length);
|
|
526
|
+
const reply = await this.sendCommand(CommandSet.ThreadReference, ThreadCommand.Frames, writer.toBuffer());
|
|
527
|
+
this.checkError(reply);
|
|
528
|
+
const reader = new JDWPReader(reply.data, this.idSizes);
|
|
529
|
+
const count = reader.readInt();
|
|
530
|
+
const frames = [];
|
|
531
|
+
for (let i = 0; i < count; i++) {
|
|
532
|
+
const frameId = reader.readFrameID();
|
|
533
|
+
const location = reader.readLocation();
|
|
534
|
+
frames.push({ frameId, location });
|
|
535
|
+
}
|
|
536
|
+
// Resolve class/method names and line numbers
|
|
537
|
+
for (const frame of frames) {
|
|
538
|
+
try {
|
|
539
|
+
const sig = await this.getReferenceTypeSignature(frame.location.classID);
|
|
540
|
+
frame.className = jniSignatureToClassName(sig);
|
|
541
|
+
const methods = await this.getMethods(frame.location.classID);
|
|
542
|
+
const method = methods.find((m) => m.methodID === frame.location.methodID);
|
|
543
|
+
if (method) {
|
|
544
|
+
frame.methodName = method.name;
|
|
545
|
+
// Get line number
|
|
546
|
+
try {
|
|
547
|
+
const lineTable = await this.getLineTable(frame.location.classID, frame.location.methodID);
|
|
548
|
+
let bestLine = -1;
|
|
549
|
+
for (const line of lineTable.lines) {
|
|
550
|
+
if (line.lineCodeIndex <= frame.location.index) {
|
|
551
|
+
bestLine = line.lineNumber;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (bestLine !== -1) {
|
|
555
|
+
frame.lineNumber = bestLine;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
// No line info available
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Can't resolve names
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return frames;
|
|
568
|
+
}
|
|
569
|
+
async getFrameVariables(threadId, frameId, location) {
|
|
570
|
+
// Get variable table for the method
|
|
571
|
+
let variables;
|
|
572
|
+
try {
|
|
573
|
+
variables = await this.getVariableTable(location.classID, location.methodID);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return []; // No variable info (compiled without debug info)
|
|
577
|
+
}
|
|
578
|
+
// Filter variables visible at current code index
|
|
579
|
+
const visibleVars = variables.filter((v) => {
|
|
580
|
+
const codeIndex = location.index;
|
|
581
|
+
return codeIndex >= v.codeIndex && codeIndex < v.codeIndex + BigInt(v.length);
|
|
582
|
+
});
|
|
583
|
+
if (visibleVars.length === 0)
|
|
584
|
+
return [];
|
|
585
|
+
// Build GetValues request
|
|
586
|
+
const writer = new JDWPWriter(this.idSizes);
|
|
587
|
+
writer.writeThreadID(threadId);
|
|
588
|
+
writer.writeFrameID(frameId);
|
|
589
|
+
writer.writeInt(visibleVars.length);
|
|
590
|
+
for (const v of visibleVars) {
|
|
591
|
+
writer.writeInt(v.slot);
|
|
592
|
+
writer.writeByte(signatureToTag(v.signature));
|
|
593
|
+
}
|
|
594
|
+
const reply = await this.sendCommand(CommandSet.StackFrame, StackFrameCommand.GetValues, writer.toBuffer());
|
|
595
|
+
this.checkError(reply);
|
|
596
|
+
const reader = new JDWPReader(reply.data, this.idSizes);
|
|
597
|
+
const count = reader.readInt();
|
|
598
|
+
const result = [];
|
|
599
|
+
for (let i = 0; i < count; i++) {
|
|
600
|
+
const tag = reader.readByte();
|
|
601
|
+
const jdwpValue = reader.readUntaggedValue(tag);
|
|
602
|
+
const varInfo = visibleVars[i];
|
|
603
|
+
const info = {
|
|
604
|
+
name: varInfo.name,
|
|
605
|
+
signature: varInfo.signature,
|
|
606
|
+
slot: varInfo.slot,
|
|
607
|
+
tag,
|
|
608
|
+
value: jdwpValue.value,
|
|
609
|
+
};
|
|
610
|
+
// Resolve string values
|
|
611
|
+
if (tag === Tag.String && jdwpValue.value !== null) {
|
|
612
|
+
try {
|
|
613
|
+
info.stringValue = await this.getStringValue(jdwpValue.value);
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
info.stringValue = "<error reading string>";
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Object values keep their JDWP id, so the IDE can expand them lazily
|
|
620
|
+
// (one `debug/expand` request per click) instead of dumping nothing.
|
|
621
|
+
if (isObjectTag(tag) && jdwpValue.value !== null && jdwpValue.value !== 0n) {
|
|
622
|
+
info.objectId = `${jdwpValue.value}`;
|
|
623
|
+
info.kind = tag === Tag.Array ? "array" : "object";
|
|
624
|
+
}
|
|
625
|
+
result.push(info);
|
|
626
|
+
}
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
async getStringValue(objectId) {
|
|
630
|
+
return this.sendRequest(CommandSet.StringReference, StringReferenceCommand.Value, (w) => w.writeObjectID(objectId), (r) => r.readString());
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* ReferenceType.Superclass of a class; 0n at `java.lang.Object`. Interfaces have
|
|
634
|
+
* no superclass, so JDWP answers with an error — callers treat that as the top.
|
|
635
|
+
*/
|
|
636
|
+
async getSuperclass(refTypeId) {
|
|
637
|
+
// CommandSet.ClassType (3) / ClassType.Superclass (1); the vendored constants
|
|
638
|
+
// only enumerate CommandSet, so the command id stays literal here.
|
|
639
|
+
return this.sendRequest(CommandSet.ClassType, 1, (w) => w.writeReferenceTypeID(refTypeId), (r) => r.readReferenceTypeID());
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* One object's fields, like a JSON dump: the declared class first, then every
|
|
643
|
+
* superclass, and static fields skipped (they belong to the class, not the value).
|
|
644
|
+
*/
|
|
645
|
+
async getObjectFields(objectId) {
|
|
646
|
+
// Get the reference type
|
|
647
|
+
const refWriter = new JDWPWriter(this.idSizes);
|
|
648
|
+
refWriter.writeObjectID(objectId);
|
|
649
|
+
const refReply = await this.sendCommand(CommandSet.ObjectReference, ObjectReferenceCommand.ReferenceType, refWriter.toBuffer());
|
|
650
|
+
this.checkError(refReply);
|
|
651
|
+
const refReader = new JDWPReader(refReply.data, this.idSizes);
|
|
652
|
+
refReader.readByte(); // refTypeTag
|
|
653
|
+
const refTypeId = refReader.readReferenceTypeID();
|
|
654
|
+
// Get fields, walking the superclass chain so inherited values show up too.
|
|
655
|
+
const fields = [];
|
|
656
|
+
let current = refTypeId;
|
|
657
|
+
for (let depth = 0; depth < 32 && current !== 0n && current !== undefined; depth += 1) {
|
|
658
|
+
let declared;
|
|
659
|
+
try {
|
|
660
|
+
declared = await this.getFields(current);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
for (const field of declared) {
|
|
666
|
+
// modBits bit 0x0008 is ACC_STATIC: not part of this instance.
|
|
667
|
+
if ((field.modBits & 0x0008) !== 0)
|
|
668
|
+
continue;
|
|
669
|
+
fields.push(field);
|
|
670
|
+
}
|
|
671
|
+
let superId;
|
|
672
|
+
try {
|
|
673
|
+
superId = await this.getSuperclass(current);
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
if (superId === undefined || superId === null || superId === 0n)
|
|
679
|
+
break;
|
|
680
|
+
current = superId;
|
|
681
|
+
}
|
|
682
|
+
if (fields.length === 0)
|
|
683
|
+
return [];
|
|
684
|
+
// Get field values
|
|
685
|
+
const valWriter = new JDWPWriter(this.idSizes);
|
|
686
|
+
valWriter.writeObjectID(objectId);
|
|
687
|
+
valWriter.writeInt(fields.length);
|
|
688
|
+
for (const f of fields) {
|
|
689
|
+
valWriter.writeFieldID(f.fieldID);
|
|
690
|
+
}
|
|
691
|
+
const valReply = await this.sendCommand(CommandSet.ObjectReference, ObjectReferenceCommand.GetValues, valWriter.toBuffer());
|
|
692
|
+
this.checkError(valReply);
|
|
693
|
+
const valReader = new JDWPReader(valReply.data, this.idSizes);
|
|
694
|
+
const valCount = valReader.readInt();
|
|
695
|
+
const result = [];
|
|
696
|
+
for (let i = 0; i < valCount; i++) {
|
|
697
|
+
const tag = valReader.readByte();
|
|
698
|
+
const jdwpValue = valReader.readUntaggedValue(tag);
|
|
699
|
+
const field = fields[i];
|
|
700
|
+
let valueStr = formatValue(tag, jdwpValue.value);
|
|
701
|
+
// Resolve string values
|
|
702
|
+
if (tag === Tag.String && jdwpValue.value !== null && jdwpValue.value !== 0n) {
|
|
703
|
+
try {
|
|
704
|
+
valueStr = `"${await this.getStringValue(jdwpValue.value)}"`;
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// keep numeric representation
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const entry = {
|
|
711
|
+
name: field.name,
|
|
712
|
+
signature: field.signature,
|
|
713
|
+
value: valueStr,
|
|
714
|
+
};
|
|
715
|
+
if (isObjectTag(tag) && jdwpValue.value !== null && jdwpValue.value !== 0n) {
|
|
716
|
+
entry.objectId = `${jdwpValue.value}`;
|
|
717
|
+
entry.kind = tag === Tag.Array ? "array" : "object";
|
|
718
|
+
}
|
|
719
|
+
result.push(entry);
|
|
720
|
+
}
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Lazily expand one object or array value (the IDE's variable tree).
|
|
725
|
+
*
|
|
726
|
+
* `fields` / `entries` carry `objectId` for nested objects, so the UI can keep
|
|
727
|
+
* expanding level by level — nothing recursive happens on the host.
|
|
728
|
+
* @param {bigint} objectId - a JDWP object id.
|
|
729
|
+
*/
|
|
730
|
+
async getObjectDetails(objectId) {
|
|
731
|
+
const refWriter = new JDWPWriter(this.idSizes);
|
|
732
|
+
refWriter.writeObjectID(objectId);
|
|
733
|
+
const refReply = await this.sendCommand(CommandSet.ObjectReference, ObjectReferenceCommand.ReferenceType, refWriter.toBuffer());
|
|
734
|
+
this.checkError(refReply);
|
|
735
|
+
const refReader = new JDWPReader(refReply.data, this.idSizes);
|
|
736
|
+
refReader.readByte();
|
|
737
|
+
const refTypeId = refReader.readReferenceTypeID();
|
|
738
|
+
const signature = await this.getReferenceTypeSignature(refTypeId);
|
|
739
|
+
const className = jniSignatureToClassName(signature);
|
|
740
|
+
if (signature.startsWith("[")) {
|
|
741
|
+
const expanded = await this.getArrayEntries(objectId);
|
|
742
|
+
return { kind: "array", className, signature, length: expanded.length, from: expanded.from, truncated: expanded.truncated, entries: expanded.entries };
|
|
743
|
+
}
|
|
744
|
+
const fields = await this.getObjectFields(objectId);
|
|
745
|
+
return { kind: "object", className, signature, fields };
|
|
746
|
+
}
|
|
747
|
+
async getArrayEntries(arrayId, firstIndex = 0, length) {
|
|
748
|
+
const lenWriter = new JDWPWriter(this.idSizes);
|
|
749
|
+
lenWriter.writeObjectID(arrayId);
|
|
750
|
+
const lenReply = await this.sendCommand(CommandSet.ArrayReference, ArrayReferenceCommand.Length, lenWriter.toBuffer());
|
|
751
|
+
this.checkError(lenReply);
|
|
752
|
+
const lenReader = new JDWPReader(lenReply.data, this.idSizes);
|
|
753
|
+
const arrayLength = lenReader.readInt();
|
|
754
|
+
const actualLength = length ?? Math.min(arrayLength - firstIndex, 100);
|
|
755
|
+
const entries = [];
|
|
756
|
+
if (actualLength > 0) {
|
|
757
|
+
const writer = new JDWPWriter(this.idSizes);
|
|
758
|
+
writer.writeObjectID(arrayId);
|
|
759
|
+
writer.writeInt(firstIndex);
|
|
760
|
+
writer.writeInt(actualLength);
|
|
761
|
+
const reply = await this.sendCommand(CommandSet.ArrayReference, ArrayReferenceCommand.GetValues, writer.toBuffer());
|
|
762
|
+
this.checkError(reply);
|
|
763
|
+
const reader = new JDWPReader(reply.data, this.idSizes);
|
|
764
|
+
const tag = reader.readByte();
|
|
765
|
+
const count = reader.readInt();
|
|
766
|
+
const isPrimitive = [Tag.Byte, Tag.Boolean, Tag.Char, Tag.Short, Tag.Int, Tag.Long, Tag.Float, Tag.Double].includes(tag);
|
|
767
|
+
for (let i = 0; i < count; i++) {
|
|
768
|
+
const entryTag = isPrimitive ? tag : reader.readByte();
|
|
769
|
+
const value = reader.readUntaggedValue(entryTag);
|
|
770
|
+
const entry = { index: firstIndex + i, tag: entryTag, value: formatValue(entryTag, value.value) };
|
|
771
|
+
if (entryTag === Tag.String && value.value !== null && value.value !== 0n) {
|
|
772
|
+
try {
|
|
773
|
+
entry.stringValue = await this.getStringValue(value.value);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
// keep the numeric/distribution representation
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (isObjectTag(entryTag) && value.value !== null && value.value !== 0n) {
|
|
780
|
+
entry.objectId = `${value.value}`;
|
|
781
|
+
entry.kind = entryTag === Tag.Array ? "array" : "object";
|
|
782
|
+
}
|
|
783
|
+
entries.push(entry);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return { length: arrayLength, from: firstIndex, entries, truncated: firstIndex + entries.length < arrayLength };
|
|
787
|
+
}
|
|
788
|
+
async getArrayValues(arrayId, firstIndex = 0, length) {
|
|
789
|
+
// Get array length first
|
|
790
|
+
const lenWriter = new JDWPWriter(this.idSizes);
|
|
791
|
+
lenWriter.writeObjectID(arrayId);
|
|
792
|
+
const lenReply = await this.sendCommand(CommandSet.ArrayReference, ArrayReferenceCommand.Length, lenWriter.toBuffer());
|
|
793
|
+
this.checkError(lenReply);
|
|
794
|
+
const lenReader = new JDWPReader(lenReply.data, this.idSizes);
|
|
795
|
+
const arrayLength = lenReader.readInt();
|
|
796
|
+
const actualLength = length ?? Math.min(arrayLength - firstIndex, 100); // Cap at 100 elements
|
|
797
|
+
if (actualLength <= 0)
|
|
798
|
+
return `[] (length: ${arrayLength})`;
|
|
799
|
+
const writer = new JDWPWriter(this.idSizes);
|
|
800
|
+
writer.writeObjectID(arrayId);
|
|
801
|
+
writer.writeInt(firstIndex);
|
|
802
|
+
writer.writeInt(actualLength);
|
|
803
|
+
const reply = await this.sendCommand(CommandSet.ArrayReference, ArrayReferenceCommand.GetValues, writer.toBuffer());
|
|
804
|
+
this.checkError(reply);
|
|
805
|
+
const reader = new JDWPReader(reply.data, this.idSizes);
|
|
806
|
+
const tag = reader.readByte();
|
|
807
|
+
const count = reader.readInt();
|
|
808
|
+
const values = [];
|
|
809
|
+
const isPrimitive = [
|
|
810
|
+
Tag.Byte,
|
|
811
|
+
Tag.Boolean,
|
|
812
|
+
Tag.Char,
|
|
813
|
+
Tag.Short,
|
|
814
|
+
Tag.Int,
|
|
815
|
+
Tag.Long,
|
|
816
|
+
Tag.Float,
|
|
817
|
+
Tag.Double,
|
|
818
|
+
].includes(tag);
|
|
819
|
+
for (let i = 0; i < count; i++) {
|
|
820
|
+
if (isPrimitive) {
|
|
821
|
+
const v = reader.readUntaggedValue(tag);
|
|
822
|
+
values.push(formatValue(tag, v.value));
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
const elemTag = reader.readByte();
|
|
826
|
+
const v = reader.readUntaggedValue(elemTag);
|
|
827
|
+
if (elemTag === Tag.String && v.value !== null && v.value !== 0n) {
|
|
828
|
+
try {
|
|
829
|
+
values.push(`"${await this.getStringValue(v.value)}"`);
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
values.push(formatValue(elemTag, v.value));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
values.push(formatValue(elemTag, v.value));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const suffix = arrayLength > actualLength + firstIndex ? `, ... (${arrayLength} total)` : "";
|
|
841
|
+
return `[${values.join(", ")}${suffix}]`;
|
|
842
|
+
}
|
|
843
|
+
// === Breakpoints ===
|
|
844
|
+
/**
|
|
845
|
+
* Resolve a class by name, waiting for it to load if not yet available.
|
|
846
|
+
* Returns the classId and the original class lookup results.
|
|
847
|
+
*/
|
|
848
|
+
async resolveClass(className) {
|
|
849
|
+
const jniSig = classNameToJNISignature(className);
|
|
850
|
+
const classes = await this.getClassesBySignature(jniSig);
|
|
851
|
+
if (classes.length === 0) {
|
|
852
|
+
const classId = await this.waitForClassPrepare(className);
|
|
853
|
+
return { classId, refTypeTag: TypeTag.Class };
|
|
854
|
+
}
|
|
855
|
+
return { classId: classes[0].typeID, refTypeTag: classes[0].refTypeTag };
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Register a breakpoint event request at a specific location and track it.
|
|
859
|
+
*/
|
|
860
|
+
async registerBreakpoint(className, line, suspendPolicy, refTypeTag, classId, methodId, codeIndex) {
|
|
861
|
+
const jdwpSuspendPolicy = suspendPolicy === "all" ? SuspendPolicy.All : SuspendPolicy.EventThread;
|
|
862
|
+
const requestId = await this.sendRequest(CommandSet.EventRequest, EventRequestCommand.Set, (w) => {
|
|
863
|
+
w.writeByte(EventKind.Breakpoint);
|
|
864
|
+
w.writeByte(jdwpSuspendPolicy);
|
|
865
|
+
w.writeInt(1); // modifier count
|
|
866
|
+
w.writeByte(ModKind.LocationOnly);
|
|
867
|
+
w.writeLocation(refTypeTag, classId, methodId, codeIndex);
|
|
868
|
+
}, (r) => r.readInt());
|
|
869
|
+
const bp = {
|
|
870
|
+
requestId,
|
|
871
|
+
className,
|
|
872
|
+
line,
|
|
873
|
+
suspendPolicy,
|
|
874
|
+
classId,
|
|
875
|
+
methodId,
|
|
876
|
+
};
|
|
877
|
+
this.breakpoints.set(requestId, bp);
|
|
878
|
+
return bp;
|
|
879
|
+
}
|
|
880
|
+
async setBreakpoint(className, line, suspendPolicy = "thread") {
|
|
881
|
+
const { classId, refTypeTag } = await this.resolveClass(className);
|
|
882
|
+
// Find the method and code index for the given line. An exact hit wins; a
|
|
883
|
+
// blank/comment/annotation line has NO code index, so fall back to the
|
|
884
|
+
// NEAREST executable line (preferring the following one) the way a normal
|
|
885
|
+
// IDE does — otherwise clicking such a line made the dot flash and vanish.
|
|
886
|
+
const methods = await this.getMethods(classId);
|
|
887
|
+
let bestMethod = null;
|
|
888
|
+
let bestCodeIndex = null;
|
|
889
|
+
let bestLine = -1;
|
|
890
|
+
let bestDistance = Infinity;
|
|
891
|
+
for (const method of methods) {
|
|
892
|
+
try {
|
|
893
|
+
const lineTable = await this.getLineTable(classId, method.methodID);
|
|
894
|
+
for (const entry of lineTable.lines) {
|
|
895
|
+
const distance = Math.abs(entry.lineNumber - line);
|
|
896
|
+
const after = entry.lineNumber >= line ? 0 : 1;
|
|
897
|
+
// Order: exact (0/0) → nearest AFTER → nearest BEFORE.
|
|
898
|
+
const score = distance === 0 ? 0 : after * 1000 + distance;
|
|
899
|
+
if (score < bestDistance) {
|
|
900
|
+
bestDistance = score;
|
|
901
|
+
bestMethod = method;
|
|
902
|
+
bestCodeIndex = entry.lineCodeIndex;
|
|
903
|
+
bestLine = entry.lineNumber;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
// Method might not have line info
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (!bestMethod || bestCodeIndex === null) {
|
|
913
|
+
throw new Error(`Cannot find any executable line in class ${className} (line ${line}). Make sure the class is compiled with debug info.`);
|
|
914
|
+
}
|
|
915
|
+
const bp = await this.registerBreakpoint(className, bestLine, suspendPolicy, refTypeTag, classId, bestMethod.methodID, bestCodeIndex);
|
|
916
|
+
// Resume VM now that breakpoint is in place (it may have been suspended by ClassPrepare)
|
|
917
|
+
if (this.threadState.vmSuspended) {
|
|
918
|
+
await this.resumeVM();
|
|
919
|
+
}
|
|
920
|
+
return bp;
|
|
921
|
+
}
|
|
922
|
+
async setBreakpointByMethod(className, methodName, suspendPolicy = "thread") {
|
|
923
|
+
const { classId, refTypeTag } = await this.resolveClass(className);
|
|
924
|
+
const methods = await this.getMethods(classId);
|
|
925
|
+
const targetMethod = methods.find((m) => m.name === methodName);
|
|
926
|
+
if (!targetMethod) {
|
|
927
|
+
const available = methods
|
|
928
|
+
.filter((m) => !m.name.startsWith("<") || m.name === "<init>")
|
|
929
|
+
.map((m) => m.name);
|
|
930
|
+
throw new Error(`Method '${methodName}' not found in class ${className}. Available methods: ${available.join(", ")}`);
|
|
931
|
+
}
|
|
932
|
+
const lineTable = await this.getLineTable(classId, targetMethod.methodID);
|
|
933
|
+
if (lineTable.lines.length === 0) {
|
|
934
|
+
throw new Error(`No line information for method '${methodName}' in class ${className}. Make sure the class is compiled with debug info.`);
|
|
935
|
+
}
|
|
936
|
+
const firstLine = lineTable.lines[0];
|
|
937
|
+
const bp = await this.registerBreakpoint(className, firstLine.lineNumber, suspendPolicy, refTypeTag, classId, targetMethod.methodID, firstLine.lineCodeIndex);
|
|
938
|
+
// Resume VM now that breakpoint is in place (it may have been suspended by ClassPrepare)
|
|
939
|
+
if (this.threadState.vmSuspended) {
|
|
940
|
+
await this.resumeVM();
|
|
941
|
+
}
|
|
942
|
+
return bp;
|
|
943
|
+
}
|
|
944
|
+
async waitForClassPrepare(className) {
|
|
945
|
+
const requestId = await this.sendRequest(CommandSet.EventRequest, EventRequestCommand.Set, (w) => {
|
|
946
|
+
w.writeByte(EventKind.ClassPrepare);
|
|
947
|
+
w.writeByte(SuspendPolicy.All);
|
|
948
|
+
w.writeInt(1); // modifier count
|
|
949
|
+
w.writeByte(ModKind.ClassMatch);
|
|
950
|
+
w.writeString(className);
|
|
951
|
+
}, (r) => r.readInt());
|
|
952
|
+
// Resume VM so the class can actually load.
|
|
953
|
+
// ClassPrepare uses SuspendPolicy.All, so VM will re-suspend when the class loads.
|
|
954
|
+
if (this.threadState.vmSuspended) {
|
|
955
|
+
await this.resumeVM();
|
|
956
|
+
}
|
|
957
|
+
return await new Promise((resolve, reject) => {
|
|
958
|
+
const timeout = setTimeout(() => {
|
|
959
|
+
this.classPrepareRequests.delete(className);
|
|
960
|
+
this.clearEventRequest(EventKind.ClassPrepare, requestId).catch(() => { });
|
|
961
|
+
reject(new Error(`Timeout waiting for class ${className} to load. Make sure the class exists and will be loaded.`));
|
|
962
|
+
}, 30000);
|
|
963
|
+
this.classPrepareRequests.set(className, {
|
|
964
|
+
resolve: (classId) => {
|
|
965
|
+
clearTimeout(timeout);
|
|
966
|
+
resolve(classId);
|
|
967
|
+
},
|
|
968
|
+
requestId,
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async removeBreakpoint(requestId) {
|
|
973
|
+
await this.clearEventRequest(EventKind.Breakpoint, requestId);
|
|
974
|
+
this.breakpoints.delete(requestId);
|
|
975
|
+
}
|
|
976
|
+
async clearAllBreakpoints() {
|
|
977
|
+
const reply = await this.sendCommand(CommandSet.EventRequest, EventRequestCommand.ClearAllBreakpoints);
|
|
978
|
+
this.checkError(reply);
|
|
979
|
+
this.breakpoints.clear();
|
|
980
|
+
}
|
|
981
|
+
async clearEventRequest(eventKind, requestId) {
|
|
982
|
+
await this.sendRequest(CommandSet.EventRequest, EventRequestCommand.Clear, (w) => {
|
|
983
|
+
w.writeByte(eventKind);
|
|
984
|
+
w.writeInt(requestId);
|
|
985
|
+
}, () => undefined);
|
|
986
|
+
}
|
|
987
|
+
getBreakpoints() {
|
|
988
|
+
return Array.from(this.breakpoints.values());
|
|
989
|
+
}
|
|
990
|
+
// === Stepping ===
|
|
991
|
+
async performStep(threadId, depth) {
|
|
992
|
+
await this.createStepRequest(threadId, StepSize.Line, depth);
|
|
993
|
+
// Resume only this thread so other suspended threads remain paused
|
|
994
|
+
this.markThreadResumed(threadId);
|
|
995
|
+
await this.resumeThread(threadId);
|
|
996
|
+
}
|
|
997
|
+
async stepOver(threadId) {
|
|
998
|
+
return this.performStep(threadId, StepDepth.Over);
|
|
999
|
+
}
|
|
1000
|
+
async stepInto(threadId) {
|
|
1001
|
+
return this.performStep(threadId, StepDepth.Into);
|
|
1002
|
+
}
|
|
1003
|
+
async stepOut(threadId) {
|
|
1004
|
+
return this.performStep(threadId, StepDepth.Out);
|
|
1005
|
+
}
|
|
1006
|
+
async createStepRequest(threadId, size, depth) {
|
|
1007
|
+
return this.sendRequest(CommandSet.EventRequest, EventRequestCommand.Set, (w) => {
|
|
1008
|
+
w.writeByte(EventKind.SingleStep);
|
|
1009
|
+
w.writeByte(SuspendPolicy.EventThread); // Only suspend the stepping thread
|
|
1010
|
+
w.writeInt(1); // modifier count
|
|
1011
|
+
w.writeByte(ModKind.Step);
|
|
1012
|
+
w.writeThreadID(threadId);
|
|
1013
|
+
w.writeInt(size);
|
|
1014
|
+
w.writeInt(depth);
|
|
1015
|
+
}, (r) => r.readInt());
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// === Helper functions ===
|
|
1019
|
+
function classNameToJNISignature(className) {
|
|
1020
|
+
// Convert "com.example.MyClass" to "Lcom/example/MyClass;"
|
|
1021
|
+
// Also handle inner classes: "com.example.MyClass$Inner" stays as is
|
|
1022
|
+
return `L${className.replace(/\./g, "/")};`;
|
|
1023
|
+
}
|
|
1024
|
+
function jniSignatureToClassName(signature) {
|
|
1025
|
+
// Convert "Lcom/example/MyClass;" to "com.example.MyClass"
|
|
1026
|
+
let s = signature;
|
|
1027
|
+
if (s.startsWith("L") && s.endsWith(";")) {
|
|
1028
|
+
s = s.substring(1, s.length - 1);
|
|
1029
|
+
}
|
|
1030
|
+
return s.replace(/\//g, ".");
|
|
1031
|
+
}
|
|
1032
|
+
function signatureToTag(signature) {
|
|
1033
|
+
switch (signature.charAt(0)) {
|
|
1034
|
+
case "B":
|
|
1035
|
+
return Tag.Byte;
|
|
1036
|
+
case "C":
|
|
1037
|
+
return Tag.Char;
|
|
1038
|
+
case "D":
|
|
1039
|
+
return Tag.Double;
|
|
1040
|
+
case "F":
|
|
1041
|
+
return Tag.Float;
|
|
1042
|
+
case "I":
|
|
1043
|
+
return Tag.Int;
|
|
1044
|
+
case "J":
|
|
1045
|
+
return Tag.Long;
|
|
1046
|
+
case "S":
|
|
1047
|
+
return Tag.Short;
|
|
1048
|
+
case "V":
|
|
1049
|
+
return Tag.Void;
|
|
1050
|
+
case "Z":
|
|
1051
|
+
return Tag.Boolean;
|
|
1052
|
+
case "L":
|
|
1053
|
+
return Tag.Object;
|
|
1054
|
+
case "[":
|
|
1055
|
+
return Tag.Array;
|
|
1056
|
+
default:
|
|
1057
|
+
return Tag.Object;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/** JDWP tags whose value is an object id (expandable in the IDE's variable tree). */
|
|
1061
|
+
function isObjectTag(tag) {
|
|
1062
|
+
return tag === Tag.Object || tag === Tag.Array || tag === Tag.Thread || tag === Tag.ThreadGroup
|
|
1063
|
+
|| tag === Tag.ClassLoader || tag === Tag.ClassObject;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function formatValue(tag, value) {
|
|
1067
|
+
if (value === null || value === undefined)
|
|
1068
|
+
return "null";
|
|
1069
|
+
// value is a primitive (number, bigint, boolean, string) from JDWP protocol reads
|
|
1070
|
+
const s = (v) => `${v}`;
|
|
1071
|
+
switch (tag) {
|
|
1072
|
+
case Tag.Boolean:
|
|
1073
|
+
return value === true ? "true" : "false";
|
|
1074
|
+
case Tag.Byte:
|
|
1075
|
+
case Tag.Short:
|
|
1076
|
+
case Tag.Int:
|
|
1077
|
+
return s(value);
|
|
1078
|
+
case Tag.Long:
|
|
1079
|
+
return `${s(value)}L`;
|
|
1080
|
+
case Tag.Float:
|
|
1081
|
+
return `${s(value)}f`;
|
|
1082
|
+
case Tag.Double:
|
|
1083
|
+
return s(value);
|
|
1084
|
+
case Tag.Char:
|
|
1085
|
+
return `'${s(value)}'`;
|
|
1086
|
+
case Tag.String:
|
|
1087
|
+
return value === 0n ? "null" : `String@${s(value)}`;
|
|
1088
|
+
case Tag.Null:
|
|
1089
|
+
return "null";
|
|
1090
|
+
case Tag.Void:
|
|
1091
|
+
return "void";
|
|
1092
|
+
case Tag.Object:
|
|
1093
|
+
case Tag.Array:
|
|
1094
|
+
case Tag.Thread:
|
|
1095
|
+
case Tag.ThreadGroup:
|
|
1096
|
+
case Tag.ClassLoader:
|
|
1097
|
+
case Tag.ClassObject:
|
|
1098
|
+
return value === 0n ? "null" : `Object@${s(value)}`;
|
|
1099
|
+
default:
|
|
1100
|
+
return s(value);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
export { formatValue, jniSignatureToClassName, classNameToJNISignature, signatureToTag };
|
|
1104
|
+
//# sourceMappingURL=client.js.map
|