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.
@@ -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 };