diff --git a/README.md b/README.md index bdfcd49..daec613 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,15 @@ honoured, so both extensions use the same SDK. terminal and the *Problems* panel (`$msCompile`). - The *.NET Solution* output channel logs every `dotnet` invocation. +## Platforms + +Windows, Linux and macOS. The only platform-specific code is around processes: the +apphost is `MyGame.Editor.exe` on Windows and `MyGame.Editor` elsewhere; the process +list comes from `wmic` (or PowerShell's CIM query where wmic is gone) on Windows and +`ps -eo pid,ppid,args` elsewhere; and stopping hot reload uses `taskkill /T` on Windows +and a process-group signal on Unix, where `dotnet watch` is started detached for that +reason. + ## Development ``` diff --git a/src/hotreload/processes.ts b/src/hotreload/processes.ts index 1f62430..fa6eaea 100644 --- a/src/hotreload/processes.ts +++ b/src/hotreload/processes.ts @@ -16,7 +16,9 @@ export interface ProcessInfo { export async function listProcesses(): Promise { if (process.platform !== 'win32') { try { - return parsePs(await capture('ps', ['-eo', 'pid=,ppid=,comm='])); + // args rather than comm: comm is the kernel's 15-character name, which would + // cut "MyGame.Editor.Tools" short and never match the assembly name. + return parsePs(await capture('ps', ['-eo', 'pid=,ppid=,args='])); } catch { return []; } @@ -67,12 +69,18 @@ export function parseWmicCsv(text: string): ProcessInfo[] { return processes; } +/** Parses `ps -eo pid=,ppid=,args=`: the name is the basename of the command, without its arguments. */ export function parsePs(text: string): ProcessInfo[] { const processes: ProcessInfo[] = []; for (const line of text.split(/\r?\n/)) { - const match = /^\s*(\d+)\s+(\d+)\s+(.*\S)\s*$/.exec(line); + const match = /^\s*(\d+)\s+(\d+)\s+(\S+)/.exec(line); if (match) { - processes.push({ pid: Number(match[1]), parentPid: Number(match[2]), name: match[3] }); + const command = match[3].replace(/^\[|\]$/g, ''); // kernel threads show as [name] + processes.push({ + pid: Number(match[1]), + parentPid: Number(match[2]), + name: command.slice(command.lastIndexOf('/') + 1), + }); } } return processes; diff --git a/src/hotreload/session.ts b/src/hotreload/session.ts index c4b2f72..19afa2e 100644 --- a/src/hotreload/session.ts +++ b/src/hotreload/session.ts @@ -111,6 +111,10 @@ export class HotReloadSession implements vscode.Disposable { cwd: spec.cwd, env, windowsHide: true, + // On Unix a detached child leads its own process group, which is what lets + // stop() kill the watcher *and* the application below it with one signal. + // Windows has taskkill /T for that and does not need it. + detached: process.platform !== 'win32', }); this.child.stdout?.on('data', chunk => this.consume(String(chunk))); diff --git a/src/test/unit/hotreload.test.ts b/src/test/unit/hotreload.test.ts index 369ba2f..fc7468e 100644 --- a/src/test/unit/hotreload.test.ts +++ b/src/test/unit/hotreload.test.ts @@ -44,6 +44,9 @@ suite('process lookup', () => { const csv = '\r\nNode,Name,ParentProcessId,ProcessId\r\nPC,dotnet.exe,1,100\r\nPC,MyGame.Editor.exe,100,103\r\n'; assert.deepStrictEqual(parseWmicCsv(csv), [ { pid: 100, parentPid: 1, name: 'dotnet.exe' }, { pid: 103, parentPid: 100, name: 'MyGame.Editor.exe' }]); + assert.deepStrictEqual(parsePs(' 100 1 /usr/bin/dotnet watch run\n 103 100 /home/u/MyGame/Bin/MyGame.Editor --flag\n 2 0 [kthreadd]\n'), [ + { pid: 100, parentPid: 1, name: 'dotnet' }, { pid: 103, parentPid: 100, name: 'MyGame.Editor' }, + { pid: 2, parentPid: 0, name: 'kthreadd' }]); assert.deepStrictEqual(parsePs(' 100 1 dotnet\n 103 100 MyGame.Editor\n'), [ { pid: 100, parentPid: 1, name: 'dotnet' }, { pid: 103, parentPid: 100, name: 'MyGame.Editor' }]); });