diff --git a/debugger/debugger.ts b/debugger/debugger.ts index dc6c4ad..927d98d 100644 --- a/debugger/debugger.ts +++ b/debugger/debugger.ts @@ -76,6 +76,27 @@ export namespace Debugger { inputFile = io.stdin; } + const pullFileEnv: LuaDebug.PullFileEnv = "LOCAL_LUA_DEBUGGER_PULL_FILE"; + const pullFilePath = os.getenv(pullFileEnv); + let lastPullSeek = 0; + let pullFile: LuaFile | null; + if (pullFilePath && pullFilePath.length > 0) { + const [file, err] = io.open(pullFilePath, "r+"); + if (!file) { + luaError(`Failed to open pull file "${pullFilePath}": ${err}\n`); + } + pullFile = file as LuaFile; + pullFile.setvbuf("no"); + const [fileSize, errorSeek] = pullFile.seek("end"); + if (!fileSize) { + luaError(`Failed to read pull file "${pullFilePath}": ${errorSeek}\n`); + } else { + lastPullSeek = fileSize; + } + } else { + pullFile = null; + } + let skipNextBreak = false; const enum HookType { @@ -475,6 +496,7 @@ export namespace Debugger { let breakAtDepth = -1; let breakInThread: Thread | undefined; let updateHook: { (): void }; + let isDebugHookDisabled = true; let ignorePatterns: string[] | undefined; let inDebugBreak = false; @@ -848,6 +870,10 @@ export namespace Debugger { const skipUnmappedLines = (os.getenv(stepUnmappedLinesEnv) !== "1"); function debugHook(event: "call" | "return" | "tail return" | "count" | "line", line?: number) { + if (isDebugHookDisabled) { + return; + } + //Stepping if (breakAtDepth >= 0) { const activeThread = getActiveThread(); @@ -1143,7 +1169,10 @@ export namespace Debugger { } updateHook = function() { - if (breakAtDepth < 0 && Breakpoint.getCount() === 0) { + isDebugHookDisabled = breakAtDepth < 0 && Breakpoint.getCount() === 0; + // Do not disable debugging in luajit environment with pull breakpoints support enabled + // or functions will be jitted and will lose debug info of lines and files + if (isDebugHookDisabled && (_G["jit"] === null || pullFile === null)) { debug.sethook(); for (const [thread] of pairs(threadIds)) { @@ -1173,6 +1202,7 @@ export namespace Debugger { coroutine.wrap = luaCoroutineWrap; coroutine.resume = luaCoroutineResume; + isDebugHookDisabled = true; debug.sethook(); for (const [thread] of pairs(threadIds)) { @@ -1251,4 +1281,14 @@ export namespace Debugger { return luaError(message, 2); } } + + export function pullBreakpoints(): void { + if (pullFile) { + const newPullSeek = pullFile.seek("end")[0] as number; + if (newPullSeek > lastPullSeek) { + lastPullSeek = newPullSeek; + triggerBreak(); + } + } + } } diff --git a/debugger/lldebugger.ts b/debugger/lldebugger.ts index a940acb..e63ea66 100644 --- a/debugger/lldebugger.ts +++ b/debugger/lldebugger.ts @@ -55,6 +55,11 @@ export function stop(): void { Debugger.clearHook(); } +//Pull breakpoints change +export function pullBreakpoints(): void { + Debugger.pullBreakpoints(); +} + //Load and debug the specified file export function runFile(filePath: unknown, breakImmediately?: boolean, arg?: unknown[]): LuaMultiReturn { if (typeof filePath !== "string") { diff --git a/debugger/protocol.d.ts b/debugger/protocol.d.ts index b56b4b4..879cea4 100644 --- a/debugger/protocol.d.ts +++ b/debugger/protocol.d.ts @@ -119,4 +119,5 @@ declare namespace LuaDebug { type StepUnmappedLinesEnv = "LOCAL_LUA_DEBUGGER_STEP_UNMAPPED_LINES"; type InputFileEnv = "LOCAL_LUA_DEBUGGER_INPUT_FILE"; type OutputFileEnv = "LOCAL_LUA_DEBUGGER_OUTPUT_FILE"; + type PullFileEnv = "LOCAL_LUA_DEBUGGER_PULL_FILE"; } diff --git a/extension/debugPipe.ts b/extension/debugPipe.ts index da8da86..c15ddc4 100644 --- a/extension/debugPipe.ts +++ b/extension/debugPipe.ts @@ -2,24 +2,34 @@ import * as crypto from "crypto"; import * as net from "net"; import * as childProcess from "child_process"; import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; export interface DebugPipe { open: (onData: (data: unknown) => void, onError: (err: unknown) => void) => void; close: () => void; write: (data: string) => void; + openPull: (onError: (err: unknown) => void) => void; + requestPull: () => void; getOutputPipePath: () => string; getInputPipePath: () => string; + getPullPipePath: () => string; } export function createNamedPipe(): DebugPipe { const pipeId = crypto.randomBytes(16).toString("hex"); const outputPipePath = `\\\\.\\pipe\\lldbg_out_${pipeId}`; const inputPipePath = `\\\\.\\pipe\\lldbg_in_${pipeId}`; + const pullPipePath = `\\\\.\\pipe\\lldbg_pull_${pipeId}`; let outputPipe: net.Server | null = null; let inputPipe: net.Server | null = null; + let pullPipe: net.Server | null = null; let inputStream: net.Socket | null; + let pullStream: net.Socket | null; + let onErrorCallback: ((err: unknown) => void) | null = null; return { open: (onData, onError) => { + onErrorCallback = onError; outputPipe = net.createServer( stream => { stream.on("data", onData); @@ -35,6 +45,21 @@ export function createNamedPipe(): DebugPipe { ); inputPipe.listen(inputPipePath); }, + openPull: (onError: (err: unknown) => void) => { + if (!onErrorCallback) { + onErrorCallback = onError; + } + + pullPipe = net.createServer( + stream => { + stream.on("error", err => { + onError(`error on pull pipe: ${err}`); + }); + pullStream = stream; + } + ); + pullPipe.listen(pullPipePath); + }, close: () => { outputPipe?.close(); @@ -42,14 +67,20 @@ export function createNamedPipe(): DebugPipe { inputPipe?.close(); inputPipe = null; inputStream = null; + pullPipe = null; + pullStream = null; }, write: data => { inputStream?.write(data); }, + requestPull: () => { + pullStream?.write("pull|\n"); + }, getOutputPipePath: () => outputPipePath, getInputPipePath: () => inputPipePath, + getPullPipePath: () => pullPipePath, }; } @@ -57,9 +88,13 @@ export function createFifoPipe(): DebugPipe { const pipeId = crypto.randomBytes(16).toString("hex"); const outputPipePath = `/tmp/lldbg_out_${pipeId}`; const inputPipePath = `/tmp/lldbg_in_${pipeId}`; - let outputFd: number | null; - let inputFd: number | null; + let pullPipePath = ""; + let debuggerTmpDir = ""; + let outputFd: number | null = null; + let inputFd: number | null = null; + let pullFd: number | null = null; let inputStream: fs.WriteStream | null = null; + let pullStream: fs.WriteStream | null = null; let onErrorCallback: ((err: unknown) => void) | null = null; return { open: (onData, onError) => { @@ -113,7 +148,27 @@ export function createFifoPipe(): DebugPipe { ); } ); + }, + openPull: (onError: (err: unknown) => void) => { + if (!onErrorCallback) { + onErrorCallback = onError; + } + + const appPrefix = "lldebugger"; + try { + debuggerTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); + pullPipePath = path.join(debuggerTmpDir, "pull.txt"); + + const fd = fs.openSync( + pullPipePath, + fs.constants.O_WRONLY | fs.constants.O_CREAT + ); + pullFd = fd; + pullStream = fs.createWriteStream(null as unknown as fs.PathLike, {fd}); + } catch (e: unknown) { + onErrorCallback(e); + } }, close: () => { @@ -141,13 +196,31 @@ export function createFifoPipe(): DebugPipe { } ); } + inputStream = null; + try { + if (pullFd !== null) { + fs.close(pullFd); + fs.rmSync(pullPipePath); + pullFd = null; + } + pullStream = null; + if (debuggerTmpDir.length > 0) { + fs.rmdirSync(debuggerTmpDir); + } + } catch (e: unknown) { + onErrorCallback?.(e); + } }, write: data => { inputStream?.write(data); }, + requestPull: () => { + pullStream?.write("pull|\n"); + }, getOutputPipePath: () => outputPipePath, getInputPipePath: () => inputPipePath, + getPullPipePath: () => pullPipePath, }; } diff --git a/extension/launchConfig.ts b/extension/launchConfig.ts index 40c6c9e..aa5bcbe 100644 --- a/extension/launchConfig.ts +++ b/extension/launchConfig.ts @@ -42,6 +42,7 @@ export interface LaunchConfig { verbose?: boolean; stopOnEntry?: boolean; breakInCoroutines?: boolean; + pullBreakpointsSupport?: boolean; stepUnmappedLines?: boolean; scriptFiles?: string[]; ignorePatterns?: string[]; diff --git a/extension/luaDebugSession.ts b/extension/luaDebugSession.ts index c887992..ba6edd3 100644 --- a/extension/luaDebugSession.ts +++ b/extension/luaDebugSession.ts @@ -76,6 +76,7 @@ const breakInCoroutinesEnv: LuaDebug.BreakInCoroutinesEnv = "LOCAL_LUA_DEBUGGER_ const stepUnmappedLinesEnv: LuaDebug.StepUnmappedLinesEnv = "LOCAL_LUA_DEBUGGER_STEP_UNMAPPED_LINES"; const inputFileEnv: LuaDebug.InputFileEnv = "LOCAL_LUA_DEBUGGER_INPUT_FILE"; const outputFileEnv: LuaDebug.OutputFileEnv = "LOCAL_LUA_DEBUGGER_OUTPUT_FILE"; +const pullFileEnv: LuaDebug.PullFileEnv = "LOCAL_LUA_DEBUGGER_PULL_FILE"; function getEnvKey(env: NodeJS.ProcessEnv, searchKey: string) { const upperSearchKey = searchKey.toUpperCase(); @@ -146,6 +147,9 @@ export class LuaDebugSession extends LoggingDebugSession { private autoContinueNext = false; private readonly activeThreads = new Map(); private isRunning = false; + private inDebuggerBreakpoint = false; + private pullBreakpointsSupport = false; + private usePipeCommutication = false; public constructor() { super("lldebugger-log.txt"); @@ -223,6 +227,10 @@ export class LuaDebugSession extends LoggingDebugSession { } } + if (typeof this.config.pullBreakpointsSupport !== "undefined") { + this.pullBreakpointsSupport = this.config.pullBreakpointsSupport; + } + //Set an environment variable so the debugger can detect the attached extension processOptions.env[envVariable] = "1"; processOptions.env[filePathEnvVariable] @@ -239,20 +247,30 @@ export class LuaDebugSession extends LoggingDebugSession { processOptions.env[stepUnmappedLinesEnv] = this.config.stepUnmappedLines ? "1" : "0"; } + this.usePipeCommutication = this.config.program.communication === "pipe"; + //Open pipes - if (this.config.program.communication === "pipe") { + if (this.usePipeCommutication || this.pullBreakpointsSupport) { if (process.platform === "win32") { this.debugPipe = createNamedPipe(); } else { this.debugPipe = createFifoPipe(); } - this.debugPipe.open( - data => { void this.onDebuggerOutput(data); }, - err => { this.showOutput(`${err}`, OutputCategory.Error); } - ); - processOptions.env[outputFileEnv] = this.debugPipe.getOutputPipePath(); - processOptions.env[inputFileEnv] = this.debugPipe.getInputPipePath(); + if (this.usePipeCommutication) { + this.debugPipe.open( + data => { void this.onDebuggerOutput(data); }, + err => { this.showOutput(`${err}`, OutputCategory.Error); } + ); + + processOptions.env[outputFileEnv] = this.debugPipe.getOutputPipePath(); + processOptions.env[inputFileEnv] = this.debugPipe.getInputPipePath(); + } + } + + if (this.pullBreakpointsSupport) { + this.debugPipe?.openPull(err => { this.showOutput(`${err}`, OutputCategory.Error); }); + processOptions.env[pullFileEnv] = this.debugPipe?.getPullPipePath(); } //Append lua path so it can find debugger script @@ -290,7 +308,7 @@ export class LuaDebugSession extends LoggingDebugSession { ); //Process callbacks - if (this.debugPipe) { + if (this.usePipeCommutication) { this.assert(this.process.stdout).on("data", data => { this.showOutput(`${data}`, OutputCategory.StdOut); }); } else { this.assert(this.process.stdout).on("data", data => { void this.onDebuggerOutput(data); }); @@ -308,6 +326,7 @@ export class LuaDebugSession extends LoggingDebugSession { this.process.on("exit", (code, signal) => this.onDebuggerTerminated(`${code !== null ? code : signal}`)); this.isRunning = true; + this.inDebuggerBreakpoint = false; this.showOutput("process launched", OutputCategory.Info); this.sendResponse(response); @@ -322,6 +341,12 @@ export class LuaDebugSession extends LoggingDebugSession { const filePath = args.source.path as string; if (this.process !== null && !this.isRunning) { + if (!this.inDebuggerBreakpoint && this.pullBreakpointsSupport) { + this.breakpointsPending = true; + this.autoContinueNext = true; + this.debugPipe?.requestPull(); + } + const oldBreakpoints = this.fileBreakpoints[filePath]; if (typeof oldBreakpoints !== "undefined") { for (const breakpoint of oldBreakpoints) { @@ -336,7 +361,13 @@ export class LuaDebugSession extends LoggingDebugSession { } } else { - this.breakpointsPending = true; + if (this.pullBreakpointsSupport && this.process !== null) { + this.breakpointsPending = true; + this.autoContinueNext = true; + this.debugPipe?.requestPull(); + } else { + this.breakpointsPending = true; + } } this.fileBreakpoints[filePath] = args.breakpoints; @@ -551,6 +582,7 @@ export class LuaDebugSession extends LoggingDebugSession { if (this.sendCommand("cont")) { this.variableHandles.reset(); this.isRunning = true; + this.inDebuggerBreakpoint = false; } else { response.success = false; } @@ -562,6 +594,7 @@ export class LuaDebugSession extends LoggingDebugSession { if (this.sendCommand("step")) { this.variableHandles.reset(); this.isRunning = true; + this.inDebuggerBreakpoint = false; } else { response.success = false; } @@ -573,6 +606,7 @@ export class LuaDebugSession extends LoggingDebugSession { if (this.sendCommand("stepin")) { this.variableHandles.reset(); this.isRunning = true; + this.inDebuggerBreakpoint = false; } else { response.success = false; } @@ -584,6 +618,7 @@ export class LuaDebugSession extends LoggingDebugSession { if (this.sendCommand("stepout")) { this.variableHandles.reset(); this.isRunning = true; + this.inDebuggerBreakpoint = false; } else { response.success = false; } @@ -679,6 +714,7 @@ export class LuaDebugSession extends LoggingDebugSession { } this.isRunning = false; + this.inDebuggerBreakpoint = false; this.sendResponse(response); } @@ -816,6 +852,8 @@ export class LuaDebugSession extends LoggingDebugSession { private async onDebuggerStop(msg: LuaDebug.DebugBreak) { this.isRunning = false; + const prevInDebugger = this.inDebuggerBreakpoint; + this.inDebuggerBreakpoint = true; if (this.pendingScripts) { for (const scriptFile of this.pendingScripts) { @@ -873,6 +911,7 @@ export class LuaDebugSession extends LoggingDebugSession { if (this.autoContinueNext) { this.autoContinueNext = false; + this.inDebuggerBreakpoint = prevInDebugger; this.assert(this.sendCommand("autocont")); } else { @@ -924,6 +963,7 @@ export class LuaDebugSession extends LoggingDebugSession { this.process = null; this.isRunning = false; + this.inDebuggerBreakpoint = false; if (this.outputText.length > 0) { this.showOutput(this.outputText, OutputCategory.StdOut); @@ -940,8 +980,8 @@ export class LuaDebugSession extends LoggingDebugSession { } this.showOutput(cmd, OutputCategory.Command); - if (this.debugPipe) { - this.debugPipe.write(`${cmd}\n`); + if (this.usePipeCommutication) { + this.debugPipe?.write(`${cmd}\n`); } else { this.assert(this.process.stdin).write(`${cmd}\n`); } diff --git a/package-lock.json b/package-lock.json index 66423e6..0aa31f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "local-lua-debugger-vscode", - "version": "0.3.1", + "version": "0.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "local-lua-debugger-vscode", - "version": "0.3.1", + "version": "0.3.3", "license": "MIT", "dependencies": { "vscode-debugadapter": "^1.48.0" diff --git a/package.json b/package.json index 5b61c09..30c6e92 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,11 @@ "description": "Break on errors inside of coroutines", "default": true }, + "pullBreakpointsSupport": { + "type": "boolean", + "description": "Runtime supports pulling of breakpoints. (need to periodically call lldebugger.pullBreakpoints())", + "default": false + }, "scriptFiles": { "type": "array", "items": {