ecalDebugAdapter.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. /**
  2. * Debug Adapter for VS Code to support the ECAL debugger.
  3. *
  4. * See 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.conect(args.host, args.port);
  162. console.log("##### launchRequest result:", response.body);
  163. this.sendResponse(response);
  164. }
  165. protected async setBreakPointsRequest(
  166. response: DebugProtocol.SetBreakpointsResponse,
  167. args: DebugProtocol.SetBreakpointsArguments
  168. ): Promise<void> {
  169. console.log("##### setBreakPointsRequest:", args);
  170. let breakpoints: DebugProtocol.Breakpoint[] = [];
  171. if (args.source.path?.indexOf(this.config.dir) === 0) {
  172. const sourcePath = args.source.path.slice(this.config.dir.length + 1);
  173. // Clear all breakpoints of the file
  174. await this.client.clearBreakpoints(sourcePath);
  175. // Send all breakpoint requests to the debug server
  176. for (const sbp of args.breakpoints || []) {
  177. await this.client.setBreakpoint(`${sourcePath}:${sbp.line}`);
  178. }
  179. // Confirm that the breakpoints have been set
  180. const status = await this.client.status();
  181. if (status) {
  182. breakpoints = (args.lines || []).map((line) => {
  183. const breakpointString = `${sourcePath}:${line}`;
  184. const bp: DebugProtocol.Breakpoint = new Breakpoint(
  185. status.breakpoints[breakpointString],
  186. line,
  187. undefined,
  188. new Source(breakpointString, args.source.path)
  189. );
  190. bp.id = this.getBreakPointId(breakpointString);
  191. return bp;
  192. });
  193. } else {
  194. breakpoints = (args.breakpoints || []).map((sbp) => {
  195. const breakpointString = `${sourcePath}:${sbp.line}`;
  196. const bp: DebugProtocol.Breakpoint = new Breakpoint(
  197. false,
  198. sbp.line,
  199. undefined,
  200. new Source(breakpointString, args.source.path)
  201. );
  202. bp.id = this.getBreakPointId(breakpointString);
  203. return bp;
  204. });
  205. this.unconfirmedBreakpoints = breakpoints;
  206. }
  207. }
  208. response.body = {
  209. breakpoints,
  210. };
  211. console.error("##### setBreakPointsRequest result:", response.body);
  212. this.sendResponse(response);
  213. }
  214. protected async breakpointLocationsRequest(
  215. response: DebugProtocol.BreakpointLocationsResponse,
  216. args: DebugProtocol.BreakpointLocationsArguments
  217. ) {
  218. let breakpoints: DebugProtocol.BreakpointLocation[] = [];
  219. if (args.source.path?.indexOf(this.config.dir) === 0) {
  220. const sourcePath = args.source.path.slice(this.config.dir.length + 1);
  221. const status = await this.client.status();
  222. if (status) {
  223. for (const [breakpointString, v] of Object.entries(
  224. status.breakpoints
  225. )) {
  226. if (v) {
  227. const line = parseInt(breakpointString.split(":")[1]);
  228. if (`${sourcePath}:${line}` === breakpointString) {
  229. breakpoints.push({
  230. line,
  231. });
  232. }
  233. }
  234. }
  235. }
  236. }
  237. response.body = {
  238. breakpoints,
  239. };
  240. this.sendResponse(response);
  241. }
  242. protected async threadsRequest(
  243. response: DebugProtocol.ThreadsResponse
  244. ): Promise<void> {
  245. console.log("##### threadsRequest");
  246. const status = await this.client.status();
  247. const threads = [];
  248. if (status) {
  249. for (const tid of Object.keys(status.threads)) {
  250. threads.push(new Thread(parseInt(tid), `Thread ${tid}`));
  251. }
  252. } else {
  253. threads.push(new Thread(1, "Thread 1"));
  254. }
  255. response.body = {
  256. threads,
  257. };
  258. console.log("##### threadsRequest result:", response.body);
  259. this.sendResponse(response);
  260. }
  261. private frameVariableScopes: Record<number, Record<string, any>> = {};
  262. private frameVariableGlobalScopes: Record<number, Record<string, any>> = {};
  263. protected async stackTraceRequest(
  264. response: DebugProtocol.StackTraceResponse,
  265. args: DebugProtocol.StackTraceArguments
  266. ) {
  267. const stackFrames: StackFrame[] = [];
  268. console.log("##### stackTraceRequest:", args);
  269. const status = await this.client.status();
  270. const threadStatus = status?.threads[String(args.threadId)];
  271. if (threadStatus?.threadRunning === false) {
  272. const ins = await this.client.describe(args.threadId);
  273. if (ins) {
  274. for (const [i, sf] of ins.callStack.entries()) {
  275. const sfNode = ins.callStackNode![i];
  276. const frameId = this.getStackFrameId(args.threadId, sf, i);
  277. const breakpointString = `${sfNode.source}:${sfNode.line}`;
  278. stackFrames.unshift(
  279. new StackFrame(
  280. frameId,
  281. sf,
  282. new Source(
  283. breakpointString,
  284. path.join(this.config.dir, sfNode.source)
  285. ),
  286. sfNode.line
  287. )
  288. );
  289. this.frameVariableScopes[frameId] = ins.callStackVsSnapshot![i];
  290. this.frameVariableGlobalScopes[
  291. frameId
  292. ] = ins.callStackVsSnapshotGlobal![i];
  293. }
  294. const frameId = this.getStackFrameId(
  295. args.threadId,
  296. ins.code!,
  297. ins.callStack.length
  298. );
  299. const breakpointString = `${ins.node!.source}:${ins.node!.line}`;
  300. stackFrames.unshift(
  301. new StackFrame(
  302. frameId,
  303. ins.code!,
  304. new Source(
  305. breakpointString,
  306. path.join(this.config.dir, ins.node!.source)
  307. ),
  308. ins.node!.line
  309. )
  310. );
  311. this.frameVariableScopes[frameId] = ins.vs!;
  312. this.frameVariableGlobalScopes[frameId] = ins.vsGlobal!;
  313. }
  314. }
  315. response.body = {
  316. stackFrames,
  317. };
  318. this.sendResponse(response);
  319. }
  320. protected scopesRequest(
  321. response: DebugProtocol.ScopesResponse,
  322. args: DebugProtocol.ScopesArguments
  323. ): void {
  324. console.error("##### scopesRequest:", args);
  325. response.body = {
  326. scopes: [
  327. new Scope("Local", this.getVariableScopeId(args.frameId, "local")),
  328. new Scope("Global", this.getVariableScopeId(args.frameId, "global")),
  329. ],
  330. };
  331. this.sendResponse(response);
  332. }
  333. protected async variablesRequest(
  334. response: DebugProtocol.VariablesResponse,
  335. args: DebugProtocol.VariablesArguments
  336. ) {
  337. console.error("##### variablesRequest", args);
  338. let vs: Record<string, any> = {};
  339. let variables: Variable[] = [];
  340. const [frameId, scopeType] = this.getScopeLookupInfo(
  341. args.variablesReference
  342. );
  343. if (scopeType === "local") {
  344. vs = this.frameVariableScopes[frameId];
  345. } else if (scopeType === "global") {
  346. vs = this.frameVariableGlobalScopes[frameId];
  347. }
  348. if (vs) {
  349. for (const [name, val] of Object.entries(vs)) {
  350. let valString: string;
  351. try {
  352. valString = JSON.stringify(val);
  353. } catch (e) {
  354. valString = String(val);
  355. }
  356. variables.push(new Variable(name, valString));
  357. }
  358. }
  359. console.log("##### variablesRequest response", variables);
  360. response.body = {
  361. variables,
  362. };
  363. this.sendResponse(response);
  364. }
  365. protected async continueRequest(
  366. response: DebugProtocol.ContinueResponse,
  367. args: DebugProtocol.ContinueArguments
  368. ) {
  369. await this.client.cont(args.threadId, ContType.Resume);
  370. response.body = {
  371. allThreadsContinued: false,
  372. };
  373. this.sendResponse(response);
  374. }
  375. protected async nextRequest(
  376. response: DebugProtocol.NextResponse,
  377. args: DebugProtocol.NextArguments
  378. ) {
  379. await this.client.cont(args.threadId, ContType.StepOver);
  380. this.sendResponse(response);
  381. }
  382. protected async stepInRequest(
  383. response: DebugProtocol.StepInResponse,
  384. args: DebugProtocol.StepInArguments
  385. ) {
  386. await this.client.cont(args.threadId, ContType.StepIn);
  387. this.sendResponse(response);
  388. }
  389. protected async stepOutRequest(
  390. response: DebugProtocol.StepOutResponse,
  391. args: DebugProtocol.StepOutArguments
  392. ) {
  393. await this.client.cont(args.threadId, ContType.StepOut);
  394. this.sendResponse(response);
  395. }
  396. protected async evaluateRequest(
  397. response: DebugProtocol.EvaluateResponse,
  398. args: DebugProtocol.EvaluateArguments
  399. ): Promise<void> {
  400. let result: any;
  401. try {
  402. result = await this.client.sendCommandString(`${args.expression}\r\n`);
  403. if (typeof result !== "string") {
  404. result = JSON.stringify(result, null, " ");
  405. }
  406. } catch (e) {
  407. result = String(e);
  408. }
  409. response.body = {
  410. result,
  411. variablesReference: 0,
  412. };
  413. this.sendResponse(response);
  414. }
  415. public shutdown() {
  416. this.client
  417. ?.shutdown()
  418. .then(() => {
  419. this.extout.appendLine("Debug Session has finished");
  420. })
  421. .catch((e) => {
  422. this.extout.appendLine(
  423. `Debug Session has finished with an error: ${e}`
  424. );
  425. });
  426. }
  427. // Id functions
  428. // ============
  429. /**
  430. * Map a given breakpoint string to a breakpoint ID.
  431. */
  432. private getBreakPointId(breakpointString: string): number {
  433. let id = this.bpIds[breakpointString];
  434. if (!id) {
  435. id = this.bpCount++;
  436. this.bpIds[breakpointString] = id;
  437. }
  438. return id;
  439. }
  440. /**
  441. * Map a given stackframe to a stackframe ID.
  442. */
  443. private getStackFrameId(
  444. threadId: string | number,
  445. frameString: string,
  446. frameIndex: number
  447. ): number {
  448. const storageString = `${threadId}###${frameString}###${frameIndex}`;
  449. let id = this.sfIds[storageString];
  450. if (!id) {
  451. id = this.sfCount++;
  452. this.sfIds[storageString] = id;
  453. }
  454. return id;
  455. }
  456. /**
  457. * Map a given variable scope to a variable scope ID.
  458. */
  459. private getVariableScopeId(frameId: number, scopeType: string): number {
  460. const storageString = `${frameId}###${scopeType}`;
  461. let id = this.vsIds[storageString];
  462. if (!id) {
  463. id = this.vsCount++;
  464. this.vsIds[storageString] = id;
  465. }
  466. return id;
  467. }
  468. /**
  469. * Map a given variable scope ID to a variable scope.
  470. */
  471. private getScopeLookupInfo(vsId: number): [number, string] {
  472. for (const [k, v] of Object.entries(this.vsIds)) {
  473. if (v === vsId) {
  474. const s = k.split("###");
  475. return [parseInt(s[0]), s[1]];
  476. }
  477. }
  478. return [-1, ""];
  479. }
  480. }
  481. class LogChannelAdapter {
  482. private out: vscode.OutputChannel;
  483. constructor(out: vscode.OutputChannel) {
  484. this.out = out;
  485. }
  486. log(value: string): void {
  487. this.out.appendLine(value);
  488. }
  489. error(value: string): void {
  490. this.out.appendLine(`Error: ${value}`);
  491. setTimeout(() => {
  492. this.out.show(true);
  493. }, 500);
  494. }
  495. }