ecalDebugAdapter.ts 15 KB

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