Linux: process-group kill and full command names from ps

dotnet watch is spawned detached on Unix so stop() can signal the whole
group; ps output uses args instead of the 15-character comm field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
This commit is contained in:
max
2026-09-08 19:30:35 +02:00
co-authored by Claude Fable 5.1
parent 0781418aaf
commit 875c486416
4 changed files with 27 additions and 3 deletions
+9
View File
@@ -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
```
+11 -3
View File
@@ -16,7 +16,9 @@ export interface ProcessInfo {
export async function listProcesses(): Promise<ProcessInfo[]> {
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;
+4
View File
@@ -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)));
+3
View File
@@ -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' }]);
});