Skip to content

Commit

Permalink
Implement inferior in its own terminal on Linux
Browse files Browse the repository at this point in the history
Using DAP's runInTerminal this PR adds the ability to use that new
terminal for the inferior's I/O.

The basic idea of the inferior terminal on Linux is:

- adapter requests client (aka vscode) to create a
  terminal (using runInTerminal)
- in that terminal we run a small script that "returns"
  the tty name to the adapter (using an atomically created
  file with the output of tty command)
- then the script waits until the adapter is complete by
  monitoring the PID of the adapter's node process

The script run in the terminal won't auto-stop when
running the adapter in server mode (typically should only
be used for development of the adapter)

Part of eclipse-cdt-cloud#161
  • Loading branch information
jonahgraham committed Feb 7, 2023
1 parent 6bf7223 commit a08cb4b
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 2 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"dependencies": {
"@vscode/debugadapter": "^1.48.0",
"@vscode/debugprotocol": "^1.48.0",
"node-addon-api": "^4.3.0"
"node-addon-api": "^4.3.0",
"tmp": "^0.2.1"
},
"devDependencies": {
"@types/chai": "^4.1.7",
Expand All @@ -81,7 +82,6 @@
"node-gyp": "^8.4.1",
"npm-run-all": "^4.1.5",
"prettier": "2.5.1",
"tmp": "^0.2.1",
"ts-node": "^10.4.0",
"typescript": "^4.5.5"
},
Expand Down
133 changes: 133 additions & 0 deletions src/GDBDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import * as tmp from 'tmp';
import {
DebugSession,
Handles,
Expand Down Expand Up @@ -50,7 +51,10 @@ export interface RequestArguments extends DebugProtocol.LaunchRequestArguments {
hardwareBreakpoint?: boolean;
}

export type InferiorTerminal = 'integrated' | 'external' | 'auto' | 'none';

export interface LaunchRequestArguments extends RequestArguments {
inferiorTerminal?: InferiorTerminal;
arguments?: string;
}

Expand Down Expand Up @@ -166,6 +170,15 @@ export class GDBDebugSession extends LoggingDebugSession {
*/
protected static frozenRequestArguments?: { request?: string };

/**
* The launch or attach request arguments passed to launchRequest
* or attachRequest available for when steps after initial
* launch need the settings.
*/
protected requestArguments?:
| LaunchRequestArguments
| AttachRequestArguments;

protected gdb: GDBBackend = this.createBackend();
protected isAttach = false;
// isRunning === true means there are no threads stopped.
Expand Down Expand Up @@ -321,6 +334,8 @@ export class GDBDebugSession extends LoggingDebugSession {
request: 'launch' | 'attach',
args: LaunchRequestArguments | AttachRequestArguments
) {
this.requestArguments = args;

logger.setup(
args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn,
args.logFile || false
Expand Down Expand Up @@ -451,6 +466,7 @@ export class GDBDebugSession extends LoggingDebugSession {
'runInTerminal',
{
kind: 'integrated',
title: this.requestArguments?.gdb || 'gdb',
cwd: process.cwd(),
env: process.env,
args: command,
Expand Down Expand Up @@ -812,6 +828,122 @@ export class GDBDebugSession extends LoggingDebugSession {
return { resolved, deletes };
}

protected async createInferiorTerminalLinux(
inferiorTerminal: InferiorTerminal
) {
if (
inferiorTerminal === 'external' ||
inferiorTerminal === 'integrated'
) {
/**
* The basic design of the inferior terminal on Linux is:
* - adapter requests client (aka vscode) to create a terminal (using runInTerminal)
* - in that terminal we run a small script that "returns" the tty name to the adapter
* - then the script waits until the adapter is complete
*
* XXX: The script run in the terminal won't auto-stop when running the adapter
* in server mode (for development)
*/

const ttyTmpDir = tmp.dirSync({
prefix: 'cdt-gdb-adapter-tty',
}).name;

fs.writeFileSync(
`${ttyTmpDir}/start-tty`,
`#!/usr/bin/env sh
echo "Terminal output from the program being debugged will appear here."
echo "GDB may display a warning about failing to set controlling terminal,"
echo "this warning can be ignored."
# Store name of tty in a temp file
tty > ${ttyTmpDir}/ttyname-temp
# rename the file for the atomic operation that
# the watcher is looking for
mv ${ttyTmpDir}/ttyname-temp ${ttyTmpDir}/ttyname
# wait for cdt-gdb-adapter to finish
# prefer using tail to detect PID exit, but that requires GNU tail
# fall back to polling if tail errors
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}/start-tty
rmdir ${ttyTmpDir}
`
);

let watcher: fs.FSWatcher | undefined;
const ttyNamePromise = new Promise<string>((resolve) => {
watcher = fs.watch(ttyTmpDir, (_eventType, filename) => {
if (filename === 'ttyname') {
watcher?.close();
resolve(
fs
.readFileSync(`${ttyTmpDir}/ttyname`)
.toString()
.trim()
);
}
});
});

const response = await new Promise<DebugProtocol.Response>(
(resolve) =>
this.sendRequest(
'runInTerminal',
{
kind: inferiorTerminal,
title: this.requestArguments?.program,
cwd: this.requestArguments?.cwd || '',
args: ['/bin/sh', `${ttyTmpDir}/start-tty`],
} as DebugProtocol.RunInTerminalRequestArguments,
5000,
resolve
)
);
if (response.success) {
const tty = await ttyNamePromise;
await this.gdb.sendCommand(`set inferior-tty ${tty}`);
return;
} else {
watcher?.close();
const message = `could not start the terminal on the client: ${response.message}`;
logger.error(message);
throw new Error(message);
}
}
}

protected async createInferiorTerminal() {
if (!this.supportsRunInTerminalRequest) {
return;
}

let inferiorTerminal =
(this.requestArguments as LaunchRequestArguments)
?.inferiorTerminal || 'none';
if (inferiorTerminal === 'auto') {
if (os.platform() === 'linux') {
inferiorTerminal = 'integrated';
} else if (os.platform() === 'win32') {
inferiorTerminal = 'external';
} else {
inferiorTerminal = 'none';
}
}

if (os.platform() === 'linux') {
await this.createInferiorTerminalLinux(inferiorTerminal);
}

// Fallthrough case is there is no inferior we can create, so simply use GDB's
// default which will make an inferior with the same I/O as GDB itself
}

protected async configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
_args: DebugProtocol.ConfigurationDoneArguments
Expand All @@ -820,6 +952,7 @@ export class GDBDebugSession extends LoggingDebugSession {
if (this.isAttach) {
await mi.sendExecContinue(this.gdb);
} else {
await this.createInferiorTerminal();
await mi.sendExecRun(this.gdb);
}
this.sendResponse(response);
Expand Down

0 comments on commit a08cb4b

Please sign in to comment.