From c0849e6ac4253e3f7bd5ba710c2482eaebd917b7 Mon Sep 17 00:00:00 2001
From: Dmitry Maslennikov <mrdaimor@gmail.com>
Date: Tue, 6 Aug 2019 17:36:15 +0300
Subject: [PATCH] debugging, attach to process

---
 package.json                                  | 18 ++++
 src/api/index.ts                              |  7 ++
 src/debug/debugConfProvider.ts                |  2 +-
 src/debug/debugSession.ts                     | 86 +++++++++++++++++--
 src/debug/xdebugConnection.ts                 | 86 ++++++++++---------
 src/extension.ts                              | 26 ++++++
 src/models.ts                                 |  7 ++
 src/providers/DocumentContentProvider.ts      |  5 +-
 src/providers/FileSystemPovider/Directory.ts  |  6 +-
 .../FileSystemPovider/FileSystemProvider.ts   | 54 ++++++------
 10 files changed, 217 insertions(+), 80 deletions(-)
 create mode 100644 src/models.ts

diff --git a/package.json b/package.json
index ca1f9a31..013edfa5 100644
--- a/package.json
+++ b/package.json
@@ -559,8 +559,26 @@
                 "description": "Absolute path to the program."
               }
             }
+          },
+          "attach": {
+            "required": [],
+            "properties": {
+              "processId": {
+                "type": ["number", "string"],
+                "description": "ID of process to attach to.",
+                "default": "${command:PickProcess}"
+              },
+              "system": {
+                "type": "boolean",
+                "description": "Enable to attach to system process.",
+                "default": false
+              }
+            }
           }
         },
+        "variables": {
+          "PickProcess": "vscode-objectscript.pickProcess"
+        },
         "initialConfigurations": [
           {
             "type": "objectscript",
diff --git a/src/api/index.ts b/src/api/index.ts
index 739cf5f1..6b421540 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -302,4 +302,11 @@ export class AtelierAPI {
       includes,
     });
   }
+  // v1+
+  public getJobs(system: boolean) {
+    const params = {
+      system,
+    };
+    return this.request(1, "GET", `%SYS/jobs`, null, params);
+  }
 }
diff --git a/src/debug/debugConfProvider.ts b/src/debug/debugConfProvider.ts
index a9dbd96f..efeafe0e 100644
--- a/src/debug/debugConfProvider.ts
+++ b/src/debug/debugConfProvider.ts
@@ -30,7 +30,7 @@ export class ObjectScriptConfigurationProvider implements DebugConfigurationProv
       }
     }
 
-    if (!config.program) {
+    if (config.request === "launch" && !config.program) {
       return vscode.window.showInformationMessage("Cannot find a program to debug").then(_ => {
         return undefined; // abort launch
       });
diff --git a/src/debug/debugSession.ts b/src/debug/debugSession.ts
index d6af7519..8a885504 100644
--- a/src/debug/debugSession.ts
+++ b/src/debug/debugSession.ts
@@ -28,8 +28,13 @@ interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
   program: string;
   /** Automatically stop target after launch. If not specified, target does not stop. */
   stopOnEntry?: boolean;
-  /** enable logging the Debug Adapter Protocol */
-  trace?: boolean;
+}
+
+interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
+  /** The process id to attach to. */
+  processId: string;
+  /** Automatically stop target after connect. If not specified, target does not stop. */
+  stopOnEntry?: boolean;
 }
 
 /** converts a local path from VS Code to a server-side XDebug file URI with respect to source root settings */
@@ -95,10 +100,12 @@ export class ObjectScriptDebugSession extends LoggingDebugSession {
     args: DebugProtocol.InitializeRequestArguments
   ): Promise<void> {
     // build and return the capabilities of this debug adapter:
-    response.body = response.body || {
-      supportsConfigurationDoneRequest: false,
+    response.body = {
+      ...response.body,
+      supportsConfigurationDoneRequest: true,
       supportsEvaluateForHovers: true,
-      supportsSetVariable: false, // TODO:
+      supportsSetVariable: false, // TODO
+      supportsConditionalBreakpoints: false, // TODO
       supportsStepBack: false,
     };
 
@@ -120,6 +127,11 @@ export class ObjectScriptDebugSession extends LoggingDebugSession {
 
     await this._connection.waitForInitPacket();
 
+    await this._connection.sendFeatureSetCommand("max_data", 8192);
+    await this._connection.sendFeatureSetCommand("max_children", 32);
+    await this._connection.sendFeatureSetCommand("max_depth", 2);
+    await this._connection.sendFeatureSetCommand("notify_ok", 1);
+
     this.sendResponse(response);
 
     this.sendEvent(new InitializedEvent());
@@ -130,24 +142,76 @@ export class ObjectScriptDebugSession extends LoggingDebugSession {
 
     const debugTarget = `${this._namespace}:${args.program}`;
     await this._connection.sendFeatureSetCommand("debug_target", debugTarget);
-    await this._connection.sendFeatureSetCommand("max_data", 1000);
 
     this._debugTargetSet.notify();
 
-    // const xdebugResponse = await this._connection.sendStepIntoCommand();
+    this.sendResponse(response);
+  }
+
+  protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): Promise<void> {
+    const debugTarget = `PID:${args.processId}`;
+    await this._connection.sendFeatureSetCommand("debug_target", debugTarget);
+    this._debugTargetSet.notify();
+
+    this.sendResponse(response);
+  }
+
+  protected async pauseRequest(
+    response: DebugProtocol.PauseResponse,
+    args: DebugProtocol.PauseArguments
+  ): Promise<void> {
+    const xdebugResponse = await this._connection.sendBreakCommand();
+    await this._checkStatus(xdebugResponse);
+
+    this.sendResponse(response);
+  }
+
+  protected async configurationDoneRequest(
+    response: DebugProtocol.ConfigurationDoneResponse,
+    args: DebugProtocol.ConfigurationDoneArguments
+  ): Promise<void> {
     const xdebugResponse = await this._connection.sendRunCommand();
     await this._checkStatus(xdebugResponse);
 
     this.sendResponse(response);
   }
 
+  protected async disconnectRequest(
+    response: DebugProtocol.DisconnectResponse,
+    args: DebugProtocol.DisconnectArguments
+  ): Promise<void> {
+    if (this._connection) {
+      const stopSupported = (await this._connection.sendFeatureGetCommand("stop")).supported;
+      if (stopSupported) {
+        const xdebugResponse = await this._connection.sendStopCommand();
+        await this._checkStatus(xdebugResponse);
+      }
+
+      const detachSupported = (await this._connection.sendFeatureGetCommand("detach")).supported;
+      if (detachSupported) {
+        const xdebugResponse = await this._connection.sendDetachCommand();
+        await this._checkStatus(xdebugResponse);
+      }
+    }
+
+    this.sendResponse(response);
+  }
+
   protected async setBreakPointsRequest(
     response: DebugProtocol.SetBreakpointsResponse,
     args: DebugProtocol.SetBreakpointsArguments
   ): Promise<void> {
+    await this._debugTargetSet.wait(1000);
+
     const filePath = args.source.path;
     const fileUri = await convertClientPathToDebugger(args.source.path, this._namespace);
 
+    // const currentList = (await this._connection.sendBreakpointListCommand()).breakpoints.filter(breakpoint => {
+    //   if (breakpoint instanceof xdebug.LineBreakpoint) {
+    //     return breakpoint.fileUri === fileUri;
+    //   }
+    // });
+
     let xdebugBreakpoints: (xdebug.ConditionalBreakpoint | xdebug.ClassLineBreakpoint | xdebug.LineBreakpoint)[] = [];
     xdebugBreakpoints = await Promise.all(
       args.breakpoints.map(async breakpoint => {
@@ -167,7 +231,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession {
             }
           });
         } else {
-          return new xdebug.LineBreakpoint(fileUri, line - 1);
+          return new xdebug.LineBreakpoint(fileUri, line);
         }
       })
     );
@@ -374,14 +438,16 @@ export class ObjectScriptDebugSession extends LoggingDebugSession {
     response: DebugProtocol.ContinueResponse,
     args: DebugProtocol.ContinueArguments
   ): Promise<void> {
+    this.sendResponse(response);
+
     const xdebugResponse = await this._connection.sendRunCommand();
     this._checkStatus(xdebugResponse);
-    this.sendResponse(response);
   }
 
   protected async nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): Promise<void> {
     const xdebugResponse = await this._connection.sendStepOverCommand();
     this._checkStatus(xdebugResponse);
+
     this.sendResponse(response);
   }
 
@@ -391,6 +457,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession {
   ): Promise<void> {
     const xdebugResponse = await this._connection.sendStepIntoCommand();
     this._checkStatus(xdebugResponse);
+
     this.sendResponse(response);
   }
 
@@ -400,6 +467,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession {
   ): Promise<void> {
     const xdebugResponse = await this._connection.sendStepOutCommand();
     this._checkStatus(xdebugResponse);
+
     this.sendResponse(response);
   }
 
diff --git a/src/debug/xdebugConnection.ts b/src/debug/xdebugConnection.ts
index d579ccc7..183084da 100644
--- a/src/debug/xdebugConnection.ts
+++ b/src/debug/xdebugConnection.ts
@@ -116,7 +116,7 @@ export class StatusResponse extends Response {
   }
 }
 
-export type BreakpointType = "line" | "call" | "return" | "exception" | "conditional" | "watch";
+export type BreakpointType = "line" | "return" | "conditional" | "watch";
 export type BreakpointState = "enabled" | "disabled";
 
 /** Abstract base class for all breakpoints */
@@ -130,9 +130,6 @@ export abstract class Breakpoint {
       case "conditional":
         // eslint-disable-next-line @typescript-eslint/no-use-before-define
         return new ConditionalBreakpoint(breakpointNode, connection);
-      case "call":
-        // eslint-disable-next-line @typescript-eslint/no-use-before-define
-        return new CallBreakpoint(breakpointNode, connection);
       default:
         throw new Error(`Invalid type ${breakpointNode.getAttribute("type")}`);
     }
@@ -159,6 +156,7 @@ export abstract class Breakpoint {
       this.state = breakpointNode.getAttribute("state") as BreakpointState;
     } else {
       this.type = rest[0];
+      this.state = "enabled";
     }
   }
   /** Removes the breakpoint by sending a breakpoint_remove command */
@@ -214,39 +212,13 @@ export class ClassLineBreakpoint extends LineBreakpoint {
   }
 }
 
-/** class for call breakpoints. Returned from a breakpoint_list or passed to sendBreakpointSetCommand */
-export class CallBreakpoint extends Breakpoint {
-  /** the function to break on */
-  public fn: string;
-  /** optional expression that must evaluate to true */
-  public expression: string;
-  /** constructs a call breakpoint from an XML node */
-  public constructor(breakpointNode: Element, connection: Connection);
-  /** contructs a call breakpoint for passing to sendSetBreakpointCommand */
-  public constructor(fn: string, expression?: string);
-  public constructor(...rest) {
-    if (typeof rest[0] === "object") {
-      const breakpointNode: Element = rest[0];
-      const connection: Connection = rest[1];
-      super(breakpointNode, connection);
-      this.fn = breakpointNode.getAttribute("function");
-      this.expression = breakpointNode.getAttribute("expression"); // Base64 encoded?
-    } else {
-      // construct from arguments
-      super("call");
-      this.fn = rest[0];
-      this.expression = rest[1];
-    }
-  }
-}
-
 /** class for conditional breakpoints. Returned from a breakpoint_list or passed to sendBreakpointSetCommand */
 export class ConditionalBreakpoint extends Breakpoint {
   /** File URI */
   public fileUri: string;
   /** Line (optional) */
   public line: number;
-  /** The PHP expression under which to break on */
+  /** The expression under which to break on */
   public expression: string;
   /** Constructs a breakpoint object from an XML node from a XDebug response */
   public constructor(breakpointNode: Element, connection: Connection);
@@ -543,6 +515,18 @@ export class FeatureSetResponse extends Response {
   }
 }
 
+export class FeatureGetResponse extends Response {
+  /** the feature that was get */
+  public feature: string;
+  /** supported flag for the feature */
+  public supported: boolean;
+  public constructor(document: XMLDocument, connection: Connection) {
+    super(document, connection);
+    this.feature = document.documentElement.getAttribute("feature");
+    this.supported = document.documentElement.getAttribute("supported") === "1";
+  }
+}
+
 /** A command inside the queue */
 interface Command {
   /** The name of the command, like breakpoint_list */
@@ -555,7 +539,7 @@ interface Command {
   resolveFn: (response: XMLDocument) => any;
   /** callback that gets called if an error happened while parsing the response */
   rejectFn: (error?: Error) => any;
-  /** whether command results in PHP code being executed or not */
+  /** whether command results in code being executed or not */
   isExecuteCommand: boolean;
 }
 
@@ -564,7 +548,7 @@ interface Command {
  */
 export class Connection extends DbgpConnection {
   /**
-   * Whether a command was started that executes PHP, which means the connection will be blocked from
+   * Whether a command was started that executes code, which means the connection will be blocked from
    * running any additional commands until the execution gets to the next stopping point or exits.
    */
   public get isPendingExecuteCommand(): boolean {
@@ -682,8 +666,8 @@ export class Connection extends DbgpConnection {
    *  - notify_ok
    * or any command.
    */
-  public async sendFeatureGetCommand(feature: string): Promise<XMLDocument> {
-    return await this._enqueueCommand("feature_get", `-n ${feature}`);
+  public async sendFeatureGetCommand(feature: string): Promise<FeatureGetResponse> {
+    return new FeatureGetResponse(await this._enqueueCommand("feature_get", `-n ${feature}`), this);
   }
 
   /**
@@ -712,8 +696,8 @@ export class Connection extends DbgpConnection {
   public async sendBreakpointSetCommand(breakpoint: Breakpoint): Promise<BreakpointSetResponse> {
     let args = `-t ${breakpoint.type}`;
     let data: string | undefined;
+    args += ` -s ${breakpoint.state}`;
     if (breakpoint instanceof LineBreakpoint) {
-      args += ` -s enabled`;
       args += ` -f ${breakpoint.fileUri}`;
       if (breakpoint instanceof ClassLineBreakpoint) {
         args += ` -m ${breakpoint.method} -n ${breakpoint.methodOffset}`;
@@ -726,9 +710,6 @@ export class Connection extends DbgpConnection {
         args += ` -n ${breakpoint.line}`;
       }
       data = breakpoint.expression;
-    } else if (breakpoint instanceof CallBreakpoint) {
-      args += ` -m ${breakpoint.fn}`;
-      data = breakpoint.expression;
     }
     return new BreakpointSetResponse(await this._enqueueCommand("breakpoint_set", args, data), this);
   }
@@ -767,7 +748,17 @@ export class Connection extends DbgpConnection {
 
   /** sends a stop command */
   public async sendStopCommand(): Promise<StatusResponse> {
-    return new StatusResponse(await this._enqueueCommand("stop"), this);
+    return new StatusResponse(await this._immediateCommand("stop"), this);
+  }
+
+  /** sends an detach command */
+  public async sendDetachCommand(): Promise<StatusResponse> {
+    return new StatusResponse(await this._immediateCommand("detach"), this);
+  }
+
+  /** sends an break command */
+  public async sendBreakCommand(): Promise<StatusResponse> {
+    return new StatusResponse(await this._immediateCommand("break"), this);
   }
 
   // ------------------------------ stack ----------------------------------------
@@ -835,8 +826,21 @@ export class Connection extends DbgpConnection {
     });
   }
 
+  private _immediateCommand(name: string, args?: string, data?: string): Promise<XMLDocument> {
+    return new Promise((resolveFn, rejectFn): void => {
+      this._executeCommand({
+        name,
+        args,
+        data,
+        resolveFn,
+        rejectFn,
+        isExecuteCommand: false,
+      });
+    });
+  }
+
   /**
-   * Pushes a new execute command (one that results in executing PHP code)
+   * Pushes a new execute command (one that results in executing code)
    * to the queue that will be executed after all the previous
    * commands have finished and we received a response.
    * If the queue is empty AND there are no pending transactions
diff --git a/src/extension.ts b/src/extension.ts
index fb33251b..5def109c 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -4,6 +4,7 @@ export const OBJECTSCRIPT_FILE_SCHEMA = "objectscript";
 export const OBJECTSCRIPTXML_FILE_SCHEMA = "objectscriptxml";
 export const FILESYSTEM_SCHEMA = "isfs";
 export const schemas = [OBJECTSCRIPT_FILE_SCHEMA, OBJECTSCRIPTXML_FILE_SCHEMA, FILESYSTEM_SCHEMA];
+import { AtelierJob } from "./models";
 
 import { importAndCompile, importFolder as importFileOrFolder, namespaceCompile } from "./commands/compile";
 import { deleteItem } from "./commands/delete";
@@ -168,6 +169,31 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
     vscode.commands.registerCommand("vscode-objectscript.compileAllWithFlags", () => namespaceCompile(true)),
     vscode.commands.registerCommand("vscode-objectscript.compileFolder", importFileOrFolder),
     vscode.commands.registerCommand("vscode-objectscript.export", exportAll),
+    vscode.commands.registerCommand("vscode-objectscript.pickProcess", async config => {
+      const system = config.system;
+      const api = new AtelierAPI();
+      const convert = data =>
+        data.result.content.map(
+          (process: AtelierJob): vscode.QuickPickItem => ({
+            label: process.pid.toString(),
+            description: `Namespace: ${process.namespace}, Routine: ${process.routine}`,
+          })
+        );
+      const list = await api.getJobs(system).then(convert);
+      if (!list.length) {
+        vscode.window.showInformationMessage("No process found to attach to", {
+          modal: true,
+        });
+        return;
+      }
+      return vscode.window
+        .showQuickPick<vscode.QuickPickItem>(list, {
+          placeHolder: "Pick the process to attach to",
+        })
+        .then(value => {
+          if (value) return value.label;
+        });
+    }),
     vscode.commands.registerCommand("vscode-objectscript.viewOthers", viewOthers),
     vscode.commands.registerCommand("vscode-objectscript.subclass", subclass),
     vscode.commands.registerCommand("vscode-objectscript.superclass", superclass),
diff --git a/src/models.ts b/src/models.ts
new file mode 100644
index 00000000..6ce9fc2a
--- /dev/null
+++ b/src/models.ts
@@ -0,0 +1,7 @@
+export interface AtelierJob {
+  pid: number;
+  namespace: string;
+  routine: string;
+  state: string;
+  device: string;
+}
diff --git a/src/providers/DocumentContentProvider.ts b/src/providers/DocumentContentProvider.ts
index ac772000..13c6d7b6 100644
--- a/src/providers/DocumentContentProvider.ts
+++ b/src/providers/DocumentContentProvider.ts
@@ -29,7 +29,10 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid
     if (found) {
       return vscode.Uri.file(found);
     }
-    const fileName = name.split(".").slice(0, -1).join("/");
+    const fileName = name
+      .split(".")
+      .slice(0, -1)
+      .join("/");
     const fileExt = name.split(".").pop();
     name = fileName + "." + fileExt;
     let uri = vscode.Uri.file(name).with({
diff --git a/src/providers/FileSystemPovider/Directory.ts b/src/providers/FileSystemPovider/Directory.ts
index 857e5f7a..05edd15f 100644
--- a/src/providers/FileSystemPovider/Directory.ts
+++ b/src/providers/FileSystemPovider/Directory.ts
@@ -2,12 +2,16 @@ import * as vscode from "vscode";
 
 import { File } from "./File";
 export class Directory implements vscode.FileStat {
+  public name: string;
+  public fullName: string;
   public type: vscode.FileType;
   public ctime: number;
   public mtime: number;
   public size: number;
   public entries: Map<string, File | Directory>;
-  constructor(public name: string, public fullName: string) {
+  public constructor(name: string, fullName: string) {
+    this.name = name;
+    this.fullName = fullName;
     this.type = vscode.FileType.Directory;
     this.ctime = Date.now();
     this.mtime = Date.now();
diff --git a/src/providers/FileSystemPovider/FileSystemProvider.ts b/src/providers/FileSystemPovider/FileSystemProvider.ts
index 3e715e3f..38a96116 100644
--- a/src/providers/FileSystemPovider/FileSystemProvider.ts
+++ b/src/providers/FileSystemPovider/FileSystemProvider.ts
@@ -7,7 +7,6 @@ import { File } from "./File";
 export type Entry = File | Directory;
 
 export class FileSystemProvider implements vscode.FileSystemProvider {
-
   public root = new Directory("", "");
 
   public readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]>;
@@ -30,19 +29,22 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
     const sql = `CALL %Library.RoutineMgr_StudioOpenDialog(?,,,,,,0)`;
     const folder = uri.path === "/" ? "/" : uri.path.replace(/\//g, ".") + "/";
     const spec = folder.slice(1) + "*.cls,*.int";
-    return api.actionQuery(sql, [spec])
-      .then((data) => data.result.content || [])
-      .then((data) => data.map((item) => {
-        const name = item.Name;
-        const fullName = folder === "" ? name : folder + "/" + name;
-        if (item.IsDirectory.length) {
-          parent.entries.set(name, new Directory(name, fullName));
-          return [name, vscode.FileType.Directory];
-        } else {
-          return [name, vscode.FileType.File];
-        }
-      }))
-      .catch((error) => {
+    return api
+      .actionQuery(sql, [spec])
+      .then(data => data.result.content || [])
+      .then(data =>
+        data.map(item => {
+          const name = item.Name;
+          const fullName = folder === "" ? name : folder + "/" + name;
+          if (item.IsDirectory.length) {
+            parent.entries.set(name, new Directory(name, fullName));
+            return [name, vscode.FileType.Directory];
+          } else {
+            return [name, vscode.FileType.File];
+          }
+        })
+      )
+      .catch(error => {
         console.error(error);
       });
   }
@@ -50,18 +52,16 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
   public createDirectory(uri: vscode.Uri): void | Thenable<void> {
     const basename = path.posix.basename(uri.path);
     const dirname = uri.with({ path: path.posix.dirname(uri.path) });
-    return this._lookupAsDirectory(dirname)
-      .then((parent) => {
-        const entry = new Directory(basename, uri.path);
-        parent.entries.set(entry.name, entry);
-        parent.mtime = Date.now();
-        parent.size += 1;
-        this._fireSoon(
-          { type: vscode.FileChangeType.Changed, uri: dirname },
-          { type: vscode.FileChangeType.Created, uri },
-        );
-      });
-
+    return this._lookupAsDirectory(dirname).then(parent => {
+      const entry = new Directory(basename, uri.path);
+      parent.entries.set(entry.name, entry);
+      parent.mtime = Date.now();
+      parent.size += 1;
+      this._fireSoon(
+        { type: vscode.FileChangeType.Changed, uri: dirname },
+        { type: vscode.FileChangeType.Created, uri }
+      );
+    });
   }
 
   public async readFile(uri: vscode.Uri): Promise<Uint8Array> {
@@ -162,7 +162,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
           return entry;
         })
       )
-      .catch((error) => {
+      .catch(error => {
         throw vscode.FileSystemError.FileNotFound(uri);
       });
   }