ecalDebugClient.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /**
  2. * Debug client implementation for the ECAL debugger.
  3. */
  4. import * as net from 'net';
  5. import { EventEmitter } from 'events';
  6. import PromiseSocket from 'promise-socket';
  7. import { LogOutputStream, DebugStatus, ThreadInspection } from './types';
  8. interface BacklogCommand {
  9. cmd:string,
  10. args? :string[],
  11. }
  12. /**
  13. * Debug client for ECAL debug server.
  14. */
  15. export class ECALDebugClient extends EventEmitter {
  16. private socket : PromiseSocket<net.Socket>;
  17. private socketLock : any;
  18. private connected : boolean = false
  19. private backlog : BacklogCommand[] = []
  20. private threadInspection : Record<string, ThreadInspection> = {}
  21. /**
  22. * Create a new debug client.
  23. */
  24. public constructor (
  25. protected out: LogOutputStream
  26. ) {
  27. super()
  28. this.socket = new PromiseSocket(new net.Socket());
  29. const AsyncLock = require('async-lock');
  30. this.socketLock = new AsyncLock();
  31. }
  32. public async conect (host: string, port: number) {
  33. try {
  34. this.out.log(`Connecting to: ${host}:${port}`);
  35. await this.socket.connect({ port, host });
  36. // this.socket.setTimeout(2000);
  37. this.connected = true;
  38. this.pollEvents(); // Start emitting events
  39. } catch (e) {
  40. this.out.error(`Could not connect to debug server: ${e}`);
  41. }
  42. }
  43. public async status () : Promise<DebugStatus | null> {
  44. try {
  45. return await this.sendCommand('status') as DebugStatus;
  46. } catch (e) {
  47. this.out.error(`Could not query for status: ${e}`);
  48. return null;
  49. }
  50. }
  51. public async inspect (tid:string) : Promise<ThreadInspection | null> {
  52. try {
  53. return await this.sendCommand('inspect', [tid]) as ThreadInspection;
  54. } catch (e) {
  55. this.out.error(`Could not inspect thread ${tid}: ${e}`);
  56. return null;
  57. }
  58. }
  59. public async setBreakpoint (breakpoint: string) {
  60. try {
  61. await this.sendCommand(`break ${breakpoint}`) as DebugStatus;
  62. } catch (e) {
  63. this.out.error(`Could not set breakpoint ${breakpoint}: ${e}`);
  64. }
  65. }
  66. public async clearBreakpoints (source: string) {
  67. try {
  68. await this.sendCommand('rmbreak', [source]) as DebugStatus;
  69. } catch (e) {
  70. this.out.error(`Could not remove breakpoints for ${source}: ${e}`);
  71. }
  72. }
  73. public async shutdown () {
  74. this.connected = false;
  75. await this.socket.destroy();
  76. }
  77. /**
  78. * PollEvents is the polling loop for debug events.
  79. */
  80. private async pollEvents () {
  81. let nextLoop = 1000;
  82. try {
  83. const status = await this.status();
  84. for (const [tid, thread] of Object.entries(status?.threads || [])) {
  85. if (thread.threadRunning === false && !this.threadInspection[tid]) {
  86. console.log("#### Thread was stopped!!")
  87. // A thread was stopped inspect it
  88. let inspection : ThreadInspection = {
  89. callstack: [],
  90. threadRunning: false
  91. }
  92. try {
  93. inspection = await this.sendCommand('describe', [tid]) as ThreadInspection;
  94. } catch (e) {
  95. this.out.error(`Could not get description for ${tid}: ${e}`);
  96. }
  97. this.threadInspection[tid] = inspection;
  98. console.log("#### Description result:", inspection)
  99. this.emit('pauseOnBreakpoint', tid);
  100. }
  101. }
  102. } catch (e) {
  103. this.out.error(`Error during event loop: ${e}`);
  104. nextLoop = 5000;
  105. }
  106. if (this.connected) {
  107. setTimeout(this.pollEvents.bind(this), nextLoop);
  108. } else {
  109. this.out.log('Stop emitting events' + nextLoop);
  110. }
  111. }
  112. public async sendCommand (cmd:string, args? :string[]): Promise<any> {
  113. // Create or process the backlog depending on the connection status
  114. if (!this.connected) {
  115. this.backlog.push({
  116. cmd,
  117. args
  118. });
  119. return null;
  120. } else if (this.backlog.length > 0) {
  121. const backlog = this.backlog;
  122. this.backlog = [];
  123. for (const item of backlog) {
  124. await this.sendCommand(item.cmd, item.args);
  125. }
  126. }
  127. return await this.sendCommandString(`##${cmd} ${args ? args.join(' ') : ''}\r\n`);
  128. }
  129. public async sendCommandString (cmdString:string): Promise<any> {
  130. // Socket needs to be locked. Reading and writing to the socket is seen
  131. // by the interpreter as async (i/o bound) code. Separate calls to
  132. // sendCommand will be executed in different event loops. Without the lock
  133. // the different sendCommand calls would mix their responses.
  134. return await this.socketLock.acquire('socket', async () => {
  135. await this.socket.write(cmdString, 'utf8');
  136. let text = '';
  137. while (!text.endsWith('\n\n')) {
  138. text += await this.socket.read(1);
  139. }
  140. let res : any = {};
  141. try {
  142. res = JSON.parse(text);
  143. } catch (e) {
  144. throw (new Error(`Could not parse response: ${text} - error:${e}`));
  145. }
  146. if (res?.DebuggerError) {
  147. throw (new Error(`Unexpected internal error for command "${cmdString}": ${res.DebuggerError}`));
  148. }
  149. if (res?.EncodedOutput !== undefined) {
  150. res = Buffer.from(res.EncodedOutput, 'base64').toString('utf8');
  151. }
  152. return res;
  153. });
  154. }
  155. }