ecalDebugAdapter.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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 extension 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 frameVariableScopes: Record<number, Record<string, any>> = {};
  65. private frameVariableGlobalScopes: Record<number, Record<string, any>> = {};
  66. private bpCount: number = 1;
  67. private sfCount: number = 1;
  68. private vsCount: number = 1;
  69. private bpIds: Record<string, number> = {};
  70. private sfIds: Record<string, number> = {};
  71. private vsIds: Record<string, number> = {};
  72. /**
  73. * Create a new debug adapter which is used for one debug session.
  74. */
  75. public constructor() {
  76. super("mock-debug.txt");
  77. this.extout.appendLine("Creating Debug Session");
  78. this.client = new ECALDebugClient(new LogChannelAdapter(this.extout));
  79. // Add event handlers
  80. this.client.on("pauseOnBreakpoint", (e: ClientBreakEvent) => {
  81. this.sendEvent(new StoppedEvent("breakpoint", e.tid));
  82. });
  83. this.client.on("status", (e: DebugStatus) => {
  84. try {
  85. if (this.unconfirmedBreakpoints.length > 0) {
  86. for (const toConfirm of this.unconfirmedBreakpoints) {
  87. for (const [breakpointString, ok] of Object.entries(
  88. e.breakpoints
  89. )) {
  90. const line = parseInt(breakpointString.split(":")[1]);
  91. if (ok) {
  92. if (
  93. toConfirm.line === line &&
  94. toConfirm.source?.name === breakpointString
  95. ) {
  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. ): void {
  121. response.body = response.body || {};
  122. // The adapter implements the configurationDoneRequest.
  123. response.body.supportsConfigurationDoneRequest = true;
  124. // make VS Code send the breakpointLocations request
  125. response.body.supportsBreakpointLocationsRequest = true;
  126. // make VS Code provide "Step in Target" functionality
  127. response.body.supportsStepInTargetsRequest = true;
  128. this.sendResponse(response);
  129. this.sendEvent(new InitializedEvent());
  130. }
  131. /**
  132. * Called as part of the "configuration Done" step in the DAP. The client (e.g. VSCode) has
  133. * finished the initialization of the debug adapter.
  134. */
  135. protected configurationDoneRequest(
  136. response: DebugProtocol.ConfigurationDoneResponse,
  137. args: DebugProtocol.ConfigurationDoneArguments
  138. ): void {
  139. super.configurationDoneRequest(response, args);
  140. this.wgConfig.done();
  141. }
  142. /**
  143. * The client (e.g. VSCode) asks the debug adapter to start the debuggee communication.
  144. */
  145. protected async launchRequest(
  146. response: DebugProtocol.LaunchResponse,
  147. args: ECALDebugArguments
  148. ) {
  149. this.config = args; // Store the configuration
  150. // Setup logging either verbose or just on errors
  151. logger.setup(
  152. args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error,
  153. false
  154. );
  155. await this.wgConfig.wait(); // Wait for configuration sequence to finish
  156. this.extout.appendLine(`Configuration loaded: ${JSON.stringify(args)}`);
  157. await this.client.connect(args.host, args.port);
  158. if (args.executeOnEntry) {
  159. this.client.reload();
  160. }
  161. this.sendResponse(response);
  162. }
  163. protected async setBreakPointsRequest(
  164. response: DebugProtocol.SetBreakpointsResponse,
  165. args: DebugProtocol.SetBreakpointsArguments
  166. ): Promise<void> {
  167. let breakpoints: DebugProtocol.Breakpoint[] = [];
  168. if (args.source.path?.indexOf(this.config.dir) === 0) {
  169. const sourcePath = args.source.path.slice(this.config.dir.length + 1);
  170. // Clear all breakpoints of the file
  171. await this.client.clearBreakpoints(sourcePath);
  172. // Send all breakpoint requests to the debug server
  173. for (const sbp of args.breakpoints || []) {
  174. await this.client.setBreakpoint(`${sourcePath}:${sbp.line}`);
  175. }
  176. // Confirm that the breakpoints have been set
  177. const status = await this.client.status();
  178. if (status) {
  179. breakpoints = (args.lines || []).map((line) => {
  180. const breakpointString = `${sourcePath}:${line}`;
  181. const bp: DebugProtocol.Breakpoint = new Breakpoint(
  182. status.breakpoints[breakpointString],
  183. line,
  184. undefined,
  185. new Source(breakpointString, args.source.path)
  186. );
  187. bp.id = this.getBreakPointId(breakpointString);
  188. return bp;
  189. });
  190. } else {
  191. breakpoints = (args.breakpoints || []).map((sbp) => {
  192. const breakpointString = `${sourcePath}:${sbp.line}`;
  193. const bp: DebugProtocol.Breakpoint = new Breakpoint(
  194. false,
  195. sbp.line,
  196. undefined,
  197. new Source(breakpointString, args.source.path)
  198. );
  199. bp.id = this.getBreakPointId(breakpointString);
  200. return bp;
  201. });
  202. this.unconfirmedBreakpoints = breakpoints;
  203. }
  204. }
  205. response.body = {
  206. breakpoints,
  207. };
  208. this.sendResponse(response);
  209. }
  210. protected async breakpointLocationsRequest(
  211. response: DebugProtocol.BreakpointLocationsResponse,
  212. args: DebugProtocol.BreakpointLocationsArguments
  213. ) {
  214. let breakpoints: DebugProtocol.BreakpointLocation[] = [];
  215. if (args.source.path?.indexOf(this.config.dir) === 0) {
  216. const sourcePath = args.source.path.slice(this.config.dir.length + 1);
  217. const status = await this.client.status();
  218. if (status) {
  219. for (const [breakpointString, v] of Object.entries(
  220. status.breakpoints
  221. )) {
  222. if (v) {
  223. const line = parseInt(breakpointString.split(":")[1]);
  224. if (`${sourcePath}:${line}` === breakpointString) {
  225. breakpoints.push({
  226. line,
  227. });
  228. }
  229. }
  230. }
  231. }
  232. }
  233. response.body = {
  234. breakpoints,
  235. };
  236. this.sendResponse(response);
  237. }
  238. protected async threadsRequest(
  239. response: DebugProtocol.ThreadsResponse
  240. ): Promise<void> {
  241. const status = await this.client.status();
  242. const threads = [];
  243. if (status) {
  244. for (const tid of Object.keys(status.threads)) {
  245. threads.push(new Thread(parseInt(tid), `Thread ${tid}`));
  246. }
  247. } else {
  248. threads.push(new Thread(1, "Thread 1"));
  249. }
  250. response.body = {
  251. threads,
  252. };
  253. this.sendResponse(response);
  254. }
  255. protected async stackTraceRequest(
  256. response: DebugProtocol.StackTraceResponse,
  257. args: DebugProtocol.StackTraceArguments
  258. ) {
  259. const stackFrames: StackFrame[] = [];
  260. const status = await this.client.status();
  261. const threadStatus = status?.threads[String(args.threadId)];
  262. if (threadStatus?.threadRunning === false) {
  263. const ins = await this.client.describe(args.threadId);
  264. if (ins) {
  265. for (const [i, sf] of ins.callStack.entries()) {
  266. const sfNode = ins.callStackNode![i];
  267. const frameId = this.getStackFrameId(args.threadId, sf, i);
  268. const breakpointString = `${sfNode.source}:${sfNode.line}`;
  269. stackFrames.unshift(
  270. new StackFrame(
  271. frameId,
  272. sf,
  273. new Source(
  274. breakpointString,
  275. path.join(this.config.dir, sfNode.source)
  276. ),
  277. sfNode.line
  278. )
  279. );
  280. this.frameVariableScopes[frameId] = ins.callStackVsSnapshot![i];
  281. this.frameVariableGlobalScopes[
  282. frameId
  283. ] = ins.callStackVsSnapshotGlobal![i];
  284. }
  285. const frameId = this.getStackFrameId(
  286. args.threadId,
  287. ins.code!,
  288. ins.callStack.length
  289. );
  290. const breakpointString = `${ins.node!.source}:${ins.node!.line}`;
  291. stackFrames.unshift(
  292. new StackFrame(
  293. frameId,
  294. ins.code!,
  295. new Source(
  296. breakpointString,
  297. path.join(this.config.dir, ins.node!.source)
  298. ),
  299. ins.node!.line
  300. )
  301. );
  302. this.frameVariableScopes[frameId] = ins.vs!;
  303. this.frameVariableGlobalScopes[frameId] = ins.vsGlobal!;
  304. }
  305. }
  306. response.body = {
  307. stackFrames,
  308. };
  309. this.sendResponse(response);
  310. }
  311. protected scopesRequest(
  312. response: DebugProtocol.ScopesResponse,
  313. args: DebugProtocol.ScopesArguments
  314. ): void {
  315. response.body = {
  316. scopes: [
  317. new Scope("Local", this.getVariableScopeId(args.frameId, "local")),
  318. new Scope("Global", this.getVariableScopeId(args.frameId, "global")),
  319. ],
  320. };
  321. this.sendResponse(response);
  322. }
  323. protected async variablesRequest(
  324. response: DebugProtocol.VariablesResponse,
  325. args: DebugProtocol.VariablesArguments
  326. ) {
  327. let vs: Record<string, any> = {};
  328. let variables: Variable[] = [];
  329. const [frameId, scopeType] = this.getScopeLookupInfo(
  330. args.variablesReference
  331. );
  332. if (scopeType === "local") {
  333. vs = this.frameVariableScopes[frameId];
  334. } else if (scopeType === "global") {
  335. vs = this.frameVariableGlobalScopes[frameId];
  336. }
  337. if (vs) {
  338. for (const [name, val] of Object.entries(vs)) {
  339. let valString: string;
  340. try {
  341. valString = JSON.stringify(val);
  342. } catch (e) {
  343. valString = String(val);
  344. }
  345. variables.push(new Variable(name, valString));
  346. }
  347. }
  348. response.body = {
  349. variables,
  350. };
  351. this.sendResponse(response);
  352. }
  353. protected async continueRequest(
  354. response: DebugProtocol.ContinueResponse,
  355. args: DebugProtocol.ContinueArguments
  356. ) {
  357. await this.client.cont(args.threadId, ContType.Resume);
  358. response.body = {
  359. allThreadsContinued: false,
  360. };
  361. this.sendResponse(response);
  362. }
  363. protected async nextRequest(
  364. response: DebugProtocol.NextResponse,
  365. args: DebugProtocol.NextArguments
  366. ) {
  367. await this.client.cont(args.threadId, ContType.StepOver);
  368. this.sendResponse(response);
  369. }
  370. protected async stepInRequest(
  371. response: DebugProtocol.StepInResponse,
  372. args: DebugProtocol.StepInArguments
  373. ) {
  374. await this.client.cont(args.threadId, ContType.StepIn);
  375. this.sendResponse(response);
  376. }
  377. protected async stepOutRequest(
  378. response: DebugProtocol.StepOutResponse,
  379. args: DebugProtocol.StepOutArguments
  380. ) {
  381. await this.client.cont(args.threadId, ContType.StepOut);
  382. this.sendResponse(response);
  383. }
  384. protected async evaluateRequest(
  385. response: DebugProtocol.EvaluateResponse,
  386. args: DebugProtocol.EvaluateArguments
  387. ): Promise<void> {
  388. let result: any;
  389. try {
  390. result = await this.client.sendCommandString(`${args.expression}\r\n`);
  391. if (typeof result !== "string") {
  392. result = JSON.stringify(result, null, " ");
  393. }
  394. } catch (e) {
  395. result = String(e);
  396. }
  397. response.body = {
  398. result,
  399. variablesReference: 0,
  400. };
  401. this.sendResponse(response);
  402. }
  403. public shutdown() {
  404. this.client
  405. ?.shutdown()
  406. .then(() => {
  407. this.extout.appendLine("Debug Session has finished");
  408. })
  409. .catch((e) => {
  410. this.extout.appendLine(
  411. `Debug Session has finished with an error: ${e}`
  412. );
  413. });
  414. }
  415. // Id functions
  416. // ============
  417. /**
  418. * Map a given breakpoint string to a breakpoint ID.
  419. */
  420. private getBreakPointId(breakpointString: string): number {
  421. let id = this.bpIds[breakpointString];
  422. if (!id) {
  423. id = this.bpCount++;
  424. this.bpIds[breakpointString] = id;
  425. }
  426. return id;
  427. }
  428. /**
  429. * Map a given stackframe to a stackframe ID.
  430. */
  431. private getStackFrameId(
  432. threadId: string | number,
  433. frameString: string,
  434. frameIndex: number
  435. ): number {
  436. const storageString = `${threadId}###${frameString}###${frameIndex}`;
  437. let id = this.sfIds[storageString];
  438. if (!id) {
  439. id = this.sfCount++;
  440. this.sfIds[storageString] = id;
  441. }
  442. return id;
  443. }
  444. /**
  445. * Map a given variable scope to a variable scope ID.
  446. */
  447. private getVariableScopeId(frameId: number, scopeType: string): number {
  448. const storageString = `${frameId}###${scopeType}`;
  449. let id = this.vsIds[storageString];
  450. if (!id) {
  451. id = this.vsCount++;
  452. this.vsIds[storageString] = id;
  453. }
  454. return id;
  455. }
  456. /**
  457. * Map a given variable scope ID to a variable scope.
  458. */
  459. private getScopeLookupInfo(vsId: number): [number, string] {
  460. for (const [k, v] of Object.entries(this.vsIds)) {
  461. if (v === vsId) {
  462. const s = k.split("###");
  463. return [parseInt(s[0]), s[1]];
  464. }
  465. }
  466. return [-1, ""];
  467. }
  468. }
  469. class LogChannelAdapter {
  470. private out: vscode.OutputChannel;
  471. constructor(out: vscode.OutputChannel) {
  472. this.out = out;
  473. }
  474. log(value: string): void {
  475. this.out.appendLine(value);
  476. }
  477. error(value: string): void {
  478. this.out.appendLine(`Error: ${value}`);
  479. setTimeout(() => {
  480. this.out.show(true);
  481. }, 500);
  482. }
  483. }