diff --git a/CHANGELOG.md b/CHANGELOG.md index 47db805e..fb4d8955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Versioning]. - solve the problem of failed parsing of containers ([@henryriley0]) - Fixes #421 - Added `registerLimit` option to specify the registers to display - PR #444 ([@chenzhiy2001]) +- add integrated terminal support ([@henryriley0]) ## [0.27.0] - 2024-02-07 diff --git a/src/backend/linux/console.ts b/src/backend/linux/console.ts index 464d6974..ceb1db71 100644 --- a/src/backend/linux/console.ts +++ b/src/backend/linux/console.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; export function spawnTerminalEmulator(preferedEmulator: string): Thenable { return new Promise((resolve, reject) => { const ttyFileOutput = "/tmp/vscode-gdb-tty-0" + Math.floor(Math.random() * 100000000).toString(36); - ChildProcess.spawn(preferedEmulator || "x-terminal-emulator", ["-e", "sh -c \"tty > " + ttyFileOutput + " && sleep 4294967294\""]); + ChildProcess.spawn("x-terminal-emulator", ["-e", "sh -c \"tty > " + ttyFileOutput + " && sleep 4294967294\""]); let it = 0; const interval = setInterval(() => { if (fs.existsSync(ttyFileOutput)) { diff --git a/src/backend/mi2/mi2.ts b/src/backend/mi2/mi2.ts index 5b91e593..ceaf1f13 100644 --- a/src/backend/mi2/mi2.ts +++ b/src/backend/mi2/mi2.ts @@ -6,6 +6,7 @@ import * as linuxTerm from '../linux/console'; import * as net from "net"; import * as fs from "fs"; import * as path from "path"; +import { DebugProtocol } from 'vscode-debugprotocol'; import { Client, ClientChannel, ExecOptions } from "ssh2"; export function escape(str: string) { @@ -116,19 +117,20 @@ export class MI2 extends EventEmitter implements IBackend { resolve(undefined); }, reject); } else { - if (separateConsole !== undefined) { + if (separateConsole == "external") { linuxTerm.spawnTerminalEmulator(separateConsole).then(tty => { promises.push(this.sendCommand("inferior-tty-set " + tty)); promises.push(...autorun.map(value => { return this.sendUserInput(value); })); Promise.all(promises).then(() => { - this.emit("debug-ready"); + if(this.application.includes('gdb')){ + this.emit("debug-ready"); + } resolve(undefined); }, reject); }); } else { promises.push(...autorun.map(value => { return this.sendUserInput(value); })); Promise.all(promises).then(() => { - this.emit("debug-ready"); resolve(undefined); }, reject); } diff --git a/src/gdb.ts b/src/gdb.ts index c1e178c0..89120031 100644 --- a/src/gdb.ts +++ b/src/gdb.ts @@ -44,6 +44,7 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum } class GDBDebugSession extends MI2DebugSession { + protected supportsRunInTerminalRequest = false; protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsGotoTargetsRequest = true; response.body.supportsHitConditionalBreakpoints = true; @@ -54,6 +55,7 @@ class GDBDebugSession extends MI2DebugSession { response.body.supportsSetVariable = true; response.body.supportsStepBack = true; response.body.supportsLogPoints = true; + args.supportsRunInTerminalRequest = true; this.sendResponse(response); } @@ -98,6 +100,20 @@ class GDBDebugSession extends MI2DebugSession { }); } else { this.miDebugger.load(args.cwd, args.target, args.arguments, args.terminal, args.autorun || []).then(() => { + if(this.miDebugger.application.includes('gdb')){ + if(args.terminal === 'integrated' || args.terminal === '' || args.terminal === undefined){ + const terminalRequestArgs:DebugProtocol.RunInTerminalRequestArguments = { + kind: "integrated", + title: this.miDebugger.application, + cwd: args.cwd || '', + args: [], + }; + if(process.platform != "win32"){ + this.createIntegratedTerminalLinux(terminalRequestArgs); + this.emit("debug-ready"); + } + } + } this.sendResponse(response); }, err => { this.sendErrorResponse(response, 103, `Failed to load MI Debugger: ${err.toString()}`); diff --git a/src/mibase.ts b/src/mibase.ts index ba3fbeb5..d891a022 100644 --- a/src/mibase.ts +++ b/src/mibase.ts @@ -776,7 +776,80 @@ export class MI2DebugSession extends DebugSession { this.sourceFileMap = new SourceFileMap(configMap, fallbackGDB); } } + public async createIntegratedTerminalLinux(args:DebugProtocol.RunInTerminalRequestArguments) { + const mkdirAsync = fs.promises.mkdir; + const mkdtempAsync = async (tempDir: string, prefix: string): Promise => { + const name = `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e9)}`; + const newDirPath = `${tempDir}/${name}`; + try { + await mkdirAsync(newDirPath, { recursive: true }); + return newDirPath; + } catch (err) { + throw new Error(`Error creating temp directory: ${err.message}`); + } + }; + const ttyTmpDir = await mkdtempAsync(os.tmpdir(), 'debug'); + + (async () => { + try { + fs.writeFileSync( + `${ttyTmpDir}/get-tty`, + `#!/usr/bin/env sh + # reset terminal to clear any previous states + reset + echo "The input and output of program will be here." + echo "Warning of set controlling terminal fail can be ignored." + tty > ${ttyTmpDir}/ttynameTmp + mv ${ttyTmpDir}/ttynameTmp ${ttyTmpDir}/ttyname + # wait for debug to finish + # prefer using tail to detect PID exit, but that requires GNU tail + tail -f --pid=${process.pid} /dev/null 2>/dev/null || while kill -s 0 ${process.pid} 2>/dev/null; do sleep 1s; done + # cleanup + rm ${ttyTmpDir}/ttyname + rm ${ttyTmpDir}/get-tty + rmdir ${ttyTmpDir} + ` + ); + + let watcher: fs.FSWatcher | undefined; + const ttyNamePromise = new Promise((resolve) => { + watcher = fs.watch(ttyTmpDir, (_eventType, filename) => { + if (filename === 'ttyname') { + watcher?.close(); + resolve( + fs.readFileSync(`${ttyTmpDir}/ttyname`).toString().trim() + ); + } + }); + }); + + args.args = ['/bin/sh', `${ttyTmpDir}/get-tty`]; + const response = await new Promise( + (resolve) => + this.sendRequest( + 'runInTerminal', + args, + 10000, + resolve + ) + ); + if (response.success) { + const tty = await ttyNamePromise; + await this.miDebugger.sendCommand(`inferior-tty-set ${tty}`).then(done => { + this.miDebugger.emit("debug-ready"); + }); + return; + } else { + watcher?.close(); + const message = `could not start the terminal on the client: ${response.message}`; + throw new Error(message); + } + } catch (err) { + throw new Error(`Error createIntegratedTerminalLinux: ${err.message}`); + } + })(); + } } function prettyStringArray(strings: any) {