ecalDebugAdapter.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. /**
  2. * Debug Adapter for VS Code to support the ECAL debugger.
  3. *
  4. * Based on the debugger extension guide:
  5. * https://code.visualstudio.com/api/extension-guides/debugger-extension
  6. */
  7. import {
  8. logger,
  9. Logger,
  10. LoggingDebugSession,
  11. Thread,
  12. Source,
  13. Breakpoint,
  14. InitializedEvent,
  15. BreakpointEvent,
  16. StoppedEvent,
  17. StackFrame,
  18. Scope,
  19. Variable,
  20. } from "vscode-debugadapter";
  21. import { DebugProtocol } from "vscode-debugprotocol";
  22. import { WaitGroup } from "@jpwilliams/waitgroup";
  23. import { ECALDebugClient } from "./ecalDebugClient";
  24. import * as vscode from "vscode";
  25. import { ClientBreakEvent, ContType, DebugStatus } from "./types";
  26. import * as path from "path";
  27. /**
  28. * ECALDebugArguments are the arguments which VSCode can pass to the debug adapter.
  29. * This defines the parameter which a VSCode instance using the ECAL extention can pass to the
  30. * debug adapter from a lauch configuration ('.vscode/launch.json') in a project folder.
  31. */
  32. interface ECALDebugArguments extends DebugProtocol.LaunchRequestArguments {
  33. host: string; // Host of the ECAL debug server
  34. port: number; // Port of the ECAL debug server
  35. dir: string; // Root directory for ECAL interpreter
  36. executeOnEntry?: boolean; // Flag if the debugged script should be executed when the debug session is started
  37. trace?: boolean; // Flag to enable verbose logging of the adapter protocol
  38. }
  39. /**
  40. * Debug adapter implementation.
  41. *
  42. * Uses: https://github.com/microsoft/vscode-debugadapter-node
  43. *
  44. * See the Debug Adapter Protocol (DAP) documentation:
  45. * https://microsoft.github.io/debug-adapter-protocol/overview#How_it_works
  46. */
  47. export class ECALDebugSession extends LoggingDebugSession {
  48. /**
  49. * Client to the ECAL debug server
  50. */
  51. private client: ECALDebugClient;
  52. /**
  53. * Output channel for log messages
  54. */
  55. private extout: vscode.OutputChannel = vscode.window.createOutputChannel(
  56. "ECAL Debug Session"
  57. );
  58. /**
  59. * WaitGroup to wait the finish of the configuration sequence
  60. */
  61. private wgConfig = new WaitGroup();
  62. private config: ECALDebugArguments = {} as ECALDebugArguments;
  63. private unconfirmedBreakpoints: DebugProtocol.Breakpoint[] = [];
  64. private bpCount: number = 1;
  65. private sfCount: number = 1;
  66. private vsCount: number = 1;
  67. private bpIds: Record<string, number> = {};
  68. private sfIds: Record<string, number> = {};
  69. private vsIds: Record<string, number> = {};
  70. /**
  71. * Create a new debug adapter which is used for one debug session.
  72. */
  73. public constructor() {
  74. super("mock-debug.txt");
  75. this.extout.appendLine("Creating Debug Session");
  76. this.client = new ECALDebugClient(new LogChannelAdapter(this.extout));
  77. // Add event handlers
  78. this.client.on("pauseOnBreakpoint", (e: ClientBreakEvent) => {
  79. console.log("#### send StoppedEvent event:", e.tid, typeof e.tid);
  80. this.sendEvent(new StoppedEvent("breakpoint", e.tid));
  81. });
  82. this.client.on("status", (e: DebugStatus) => {
  83. try {
  84. if (this.unconfirmedBreakpoints.length > 0) {
  85. for (const toConfirm of this.unconfirmedBreakpoints) {
  86. for (const [breakpointString, ok] of Object.entries(
  87. e.breakpoints
  88. )) {
  89. const line = parseInt(breakpointString.split(":")[1]);
  90. if (ok) {
  91. if (
  92. toConfirm.line === line &&
  93. toConfirm.source?.name === breakpointString
  94. ) {
  95. console.log("Confirmed breakpoint:", breakpointString);
  96. toConfirm.verified = true;
  97. this.sendEvent(new BreakpointEvent("changed", toConfirm));
  98. }
  99. }
  100. }
  101. }
  102. this.unconfirmedBreakpoints = [];
  103. }
  104. } catch (e) {
  105. console.error(e);
  106. }
  107. });
  108. // Lines and columns start at 1
  109. this.setDebuggerLinesStartAt1(true);
  110. this.setDebuggerColumnsStartAt1(true);
  111. // Increment the config WaitGroup counter for configurationDoneRequest()
  112. this.wgConfig.add(1);
  113. }
  114. /**
  115. * Called as the first step in the DAP. The client (e.g. VSCode)
  116. * interrogates the debug adapter on the features which it provides.
  117. */
  118. protected initializeRequest(
  119. response: DebugProtocol.InitializeResponse,
  120. args: DebugProtocol.InitializeRequestArguments
  121. ): void {
  122. console.log("##### initializeRequest:", args);
  123. response.body = response.body || {};
  124. // The adapter implements the configurationDoneRequest.
  125. response.body.supportsConfigurationDoneRequest = true;
  126. // make VS Code send the breakpointLocations request
  127. response.body.supportsBreakpointLocationsRequest = true;
  128. // make VS Code provide "Step in Target" functionality
  129. response.body.supportsStepInTargetsRequest = true;
  130. this.sendResponse(response);
  131. this.sendEvent(new InitializedEvent());
  132. }
  133. /**
  134. * Called as part of the "configuration Done" step in the DAP. The client (e.g. VSCode) has
  135. * finished the initialization of the debug adapter.
  136. */
  137. protected configurationDoneRequest(
  138. response: DebugProtocol.ConfigurationDoneResponse,
  139. args: DebugProtocol.ConfigurationDoneArguments
  140. ): void {
  141. console.log("##### configurationDoneRequest");
  142. super.configurationDoneRequest(response, args);
  143. this.wgConfig.done();
  144. }
  145. /**
  146. * The client (e.g. VSCode) asks the debug adapter to start the debuggee communication.
  147. */
  148. protected async launchRequest(
  149. response: DebugProtocol.LaunchResponse,
  150. args: ECALDebugArguments
  151. ) {
  152. console.log("##### launchRequest:", args);
  153. this.config = args; // Store the configuration
  154. // Setup logging either verbose or just on errors
  155. logger.setup(
  156. args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error,
  157. false
  158. );
  159. await this.wgConfig.wait(); // Wait for configuration sequence to finish
  160. this.extout.appendLine(`Configuration loaded: ${JSON.stringify(args)}`);
  161. await this.client.connect(args.host, args.port);
  162. if (args.executeOnEntry) {
  163. this.client.reload();
  164. }
  165. console.log("##### launchRequest result:", response.body);
  166. this.sendResponse(response);
  167. }
  168. protected async setBreakPointsRequest(
  169. response: DebugProtocol.SetBreakpointsResponse,
  170. args: DebugProtocol.SetBreakpointsArguments
  171. ): Promise<void> {
  172. console.log("##### setBreakPointsRequest:", args);
  173. let breakpoints: DebugProtocol.Breakpoint[] = [];
  174. if (args.source.path?.indexOf(this.config.dir) === 0) {
  175. const sourcePath = args.source.path.slice(this.config.dir.length + 1);
  176. // Clear all breakpoints of the file
  177. await this.client.clearBreakpoints(sourcePath);
  178. // Send all breakpoint requests to the debug server
  179. for (const sbp of args.breakpoints || []) {
  180. await this.client.setBreakpoint(`${sourcePath}:${sbp.line}`);
  181. }
  182. // Confirm that the breakpoints have been set
  183. const status = await this.client.status();
  184. if (status) {
  185. breakpoints = (args.lines || []).map((line) => {
  186. const breakpointString = `${sourcePath}:${line}`;
  187. const bp: DebugProtocol.Breakpoint = new Breakpoint(
  188. status.breakpoints[breakpointString],
  189. line,
  190. undefined,
  191. new Source(breakpointString, args.source.path)
  192. );
  193. bp.id = this.getBreakPointId(breakpointString);
  194. return bp;
  195. });
  196. } else {
  197. breakpoints = (args.breakpoints || []).map((sbp) => {
  198. const breakpointString = `${sourcePath}:${sbp.line}`;
  199. const bp: DebugProtocol.Breakpoint = new Breakpoint(
  200. false,
  201. sbp.line,
  202. undefined,
  203. new Source(breakpointString, args.source.path)
  204. );
  205. bp.id = this.getBreakPointId(breakpointString);
  206. return bp;
  207. });
  208. this.unconfirmedBreakpoints = breakpoints;
  209. }
  210. }
  211. response.body = {
  212. breakpoints,
  213. };
  214. console.error("##### setBreakPointsRequest result:", response.body);
  215. this.sendResponse(response);
  216. }
  217. protected async breakpointLocationsRequest(
  218. response: DebugProtocol.BreakpointLocationsResponse,
  219. args: DebugProtocol.BreakpointLocationsArguments
  220. ) {
  221. let breakpoints: DebugProtocol.BreakpointLocation[] = [];
  222. if (args.source.path?.indexOf(this.config.dir) === 0) {
  223. const sourcePath = args.source.path.slice(this.config.dir.length + 1);
  224. const status = await this.client.status();
  225. if (status) {
  226. for (const [breakpointString, v] of Object.entries(
  227. status.breakpoints
  228. )) {
  229. if (v) {
  230. const line = parseInt(breakpointString.split(":")[1]);
  231. if (`${sourcePath}:${line}` === breakpointString) {
  232. breakpoints.push({
  233. line,
  234. });
  235. }
  236. }
  237. }
  238. }
  239. }
  240. response.body = {
  241. breakpoints,
  242. };
  243. this.sendResponse(response);
  244. }
  245. protected async threadsRequest(
  246. response: DebugProtocol.ThreadsResponse
  247. ): Promise<void> {
  248. console.log("##### threadsRequest");
  249. const status = await this.client.status();
  250. const threads = [];
  251. if (status) {
  252. for (const tid of Object.keys(status.threads)) {
  253. threads.push(new Thread(parseInt(tid), `Thread ${tid}`));
  254. }
  255. } else {
  256. threads.push(new Thread(1, "Thread 1"));
  257. }
  258. response.body = {
  259. threads,
  260. };
  261. console.log("##### threadsRequest result:", response.body);
  262. this.sendResponse(response);
  263. }
  264. private frameVariableScopes: Record<number, Record<string, any>> = {};
  265. private frameVariableGlobalScopes: Record<number, Record<string, any>> = {};
  266. protected async stackTraceRequest(
  267. response: DebugProtocol.StackTraceResponse,
  268. args: DebugProtocol.StackTraceArguments
  269. ) {
  270. const stackFrames: StackFrame[] = [];
  271. console.log("##### stackTraceRequest:", args);
  272. const status = await this.client.status();
  273. const threadStatus = status?.threads[String(args.threadId)];
  274. if (threadStatus?.threadRunning === false) {
  275. const ins = await this.client.describe(args.threadId);
  276. if (ins) {
  277. for (const [i, sf] of ins.callStack.entries()) {
  278. const sfNode = ins.callStackNode![i];
  279. const frameId = this.getStackFrameId(args.threadId, sf, i);
  280. const breakpointString = `${sfNode.source}:${sfNode.line}`;
  281. stackFrames.unshift(
  282. new StackFrame(
  283. frameId,
  284. sf,
  285. new Source(
  286. breakpointString,
  287. path.join(this.config.dir, sfNode.source)
  288. ),
  289. sfNode.line
  290. )
  291. );
  292. this.frameVariableScopes[frameId] = ins.callStackVsSnapshot![i];
  293. this.frameVariableGlobalScopes[
  294. frameId
  295. ] = ins.callStackVsSnapshotGlobal![i];
  296. }
  297. const frameId = this.getStackFrameId(
  298. args.threadId,
  299. ins.code!,
  300. ins.callStack.length
  301. );
  302. const breakpointString = `${ins.node!.source}:${ins.node!.line}`;
  303. stackFrames.unshift(
  304. new StackFrame(
  305. frameId,
  306. ins.code!,
  307. new Source(
  308. breakpointString,
  309. path.join(this.config.dir, ins.node!.source)
  310. ),
  311. ins.node!.line
  312. )
  313. );
  314. this.frameVariableScopes[frameId] = ins.vs!;
  315. this.frameVariableGlobalScopes[frameId] = ins.vsGlobal!;
  316. }
  317. }
  318. response.body = {
  319. stackFrames,
  320. };
  321. this.sendResponse(response);
  322. }
  323. protected scopesRequest(
  324. response: DebugProtocol.ScopesResponse,
  325. args: DebugProtocol.ScopesArguments
  326. ): void {
  327. console.error("##### scopesRequest:", args);
  328. response.body = {
  329. scopes: [
  330. new Scope("Local", this.getVariableScopeId(args.frameId, "local")),
  331. new Scope("Global", this.getVariableScopeId(args.frameId, "global")),
  332. ],
  333. };
  334. this.sendResponse(response);
  335. }
  336. protected async variablesRequest(
  337. response: DebugProtocol.VariablesResponse,
  338. args: DebugProtocol.VariablesArguments
  339. ) {
  340. console.error("##### variablesRequest", args);
  341. let vs: Record<string, any> = {};
  342. let variables: Variable[] = [];
  343. const [frameId, scopeType] = this.getScopeLookupInfo(
  344. args.variablesReference
  345. );
  346. if (scopeType === "local") {
  347. vs = this.frameVariableScopes[frameId];
  348. } else if (scopeType === "global") {
  349. vs = this.frameVariableGlobalScopes[frameId];
  350. }
  351. if (vs) {
  352. for (const [name, val] of Object.entries(vs)) {
  353. let valString: string;
  354. try {
  355. valString = JSON.stringify(val);
  356. } catch (e) {
  357. valString = String(val);
  358. }
  359. variables.push(new Variable(name, valString));
  360. }
  361. }
  362. console.log("##### variablesRequest response", variables);
  363. response.body = {
  364. variables,
  365. };
  366. this.sendResponse(response);
  367. }
  368. protected async continueRequest(
  369. response: DebugProtocol.ContinueResponse,
  370. args: DebugProtocol.ContinueArguments
  371. ) {
  372. await this.client.cont(args.threadId, ContType.Resume);
  373. response.body = {
  374. allThreadsContinued: false,
  375. };
  376. this.sendResponse(response);
  377. }
  378. protected async nextRequest(
  379. response: DebugProtocol.NextResponse,
  380. args: DebugProtocol.NextArguments
  381. ) {
  382. await this.client.cont(args.threadId, ContType.StepOver);
  383. this.sendResponse(response);
  384. }
  385. protected async stepInRequest(
  386. response: DebugProtocol.StepInResponse,
  387. args: DebugProtocol.StepInArguments
  388. ) {
  389. await this.client.cont(args.threadId, ContType.StepIn);
  390. this.sendResponse(response);
  391. }
  392. protected async stepOutRequest(
  393. response: DebugProtocol.StepOutResponse,
  394. args: DebugProtocol.StepOutArguments
  395. ) {
  396. await this.client.cont(args.threadId, ContType.StepOut);
  397. this.sendResponse(response);
  398. }
  399. protected async evaluateRequest(
  400. response: DebugProtocol.EvaluateResponse,
  401. args: DebugProtocol.EvaluateArguments
  402. ): Promise<void> {
  403. let result: any;
  404. try {
  405. result = await this.client.sendCommandString(`${args.expression}\r\n`);
  406. if (typeof result !== "string") {
  407. result = JSON.stringify(result, null, " ");
  408. }
  409. } catch (e) {
  410. result = String(e);
  411. }
  412. response.body = {
  413. result,
  414. variablesReference: 0,
  415. };
  416. this.sendResponse(response);
  417. }
  418. public shutdown() {
  419. this.client
  420. ?.shutdown()
  421. .then(() => {
  422. this.extout.appendLine("Debug Session has finished");
  423. })
  424. .catch((e) => {
  425. this.extout.appendLine(
  426. `Debug Session has finished with an error: ${e}`
  427. );
  428. });
  429. }
  430. // Id functions
  431. // ============
  432. /**
  433. * Map a given breakpoint string to a breakpoint ID.
  434. */
  435. private getBreakPointId(breakpointString: string): number {
  436. let id = this.bpIds[breakpointString];
  437. if (!id) {
  438. id = this.bpCount++;
  439. this.bpIds[breakpointString] = id;
  440. }
  441. return id;
  442. }
  443. /**
  444. * Map a given stackframe to a stackframe ID.
  445. */
  446. private getStackFrameId(
  447. threadId: string | number,
  448. frameString: string,
  449. frameIndex: number
  450. ): number {
  451. const storageString = `${threadId}###${frameString}###${frameIndex}`;
  452. let id = this.sfIds[storageString];
  453. if (!id) {
  454. id = this.sfCount++;
  455. this.sfIds[storageString] = id;
  456. }
  457. return id;
  458. }
  459. /**
  460. * Map a given variable scope to a variable scope ID.
  461. */
  462. private getVariableScopeId(frameId: number, scopeType: string): number {
  463. const storageString = `${frameId}###${scopeType}`;
  464. let id = this.vsIds[storageString];
  465. if (!id) {
  466. id = this.vsCount++;
  467. this.vsIds[storageString] = id;
  468. }
  469. return id;
  470. }
  471. /**
  472. * Map a given variable scope ID to a variable scope.
  473. */
  474. private getScopeLookupInfo(vsId: number): [number, string] {
  475. for (const [k, v] of Object.entries(this.vsIds)) {
  476. if (v === vsId) {
  477. const s = k.split("###");
  478. return [parseInt(s[0]), s[1]];
  479. }
  480. }
  481. return [-1, ""];
  482. }
  483. }
  484. class LogChannelAdapter {
  485. private out: vscode.OutputChannel;
  486. constructor(out: vscode.OutputChannel) {
  487. this.out = out;
  488. }
  489. log(value: string): void {
  490. this.out.appendLine(value);
  491. }
  492. error(value: string): void {
  493. this.out.appendLine(`Error: ${value}`);
  494. setTimeout(() => {
  495. this.out.show(true);
  496. }, 500);
  497. }
  498. }