From c0849e6ac4253e3f7bd5ba710c2482eaebd917b7 Mon Sep 17 00:00:00 2001 From: Dmitry Maslennikov 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 { // 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 { + 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 { + const xdebugResponse = await this._connection.sendBreakCommand(); + await this._checkStatus(xdebugResponse); + + this.sendResponse(response); + } + + protected async configurationDoneRequest( + response: DebugProtocol.ConfigurationDoneResponse, + args: DebugProtocol.ConfigurationDoneArguments + ): Promise { const xdebugResponse = await this._connection.sendRunCommand(); await this._checkStatus(xdebugResponse); this.sendResponse(response); } + protected async disconnectRequest( + response: DebugProtocol.DisconnectResponse, + args: DebugProtocol.DisconnectArguments + ): Promise { + 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 { + 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 { + 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 { const xdebugResponse = await this._connection.sendStepOverCommand(); this._checkStatus(xdebugResponse); + this.sendResponse(response); } @@ -391,6 +457,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { ): Promise { const xdebugResponse = await this._connection.sendStepIntoCommand(); this._checkStatus(xdebugResponse); + this.sendResponse(response); } @@ -400,6 +467,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { ): Promise { 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 { - return await this._enqueueCommand("feature_get", `-n ${feature}`); + public async sendFeatureGetCommand(feature: string): Promise { + 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 { 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 { - return new StatusResponse(await this._enqueueCommand("stop"), this); + return new StatusResponse(await this._immediateCommand("stop"), this); + } + + /** sends an detach command */ + public async sendDetachCommand(): Promise { + return new StatusResponse(await this._immediateCommand("detach"), this); + } + + /** sends an break command */ + public async sendBreakCommand(): Promise { + 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 { + 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 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(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; - 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; @@ -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 { 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 { @@ -162,7 +162,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { return entry; }) ) - .catch((error) => { + .catch(error => { throw vscode.FileSystemError.FileNotFound(uri); }); }