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