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:
@@ -250,6 +250,15 @@ honoured, so both extensions use the same SDK.
|
|||||||
terminal and the *Problems* panel (`$msCompile`).
|
terminal and the *Problems* panel (`$msCompile`).
|
||||||
- The *.NET Solution* output channel logs every `dotnet` invocation.
|
- 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
|
## Development
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export interface ProcessInfo {
|
|||||||
export async function listProcesses(): Promise<ProcessInfo[]> {
|
export async function listProcesses(): Promise<ProcessInfo[]> {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -67,12 +69,18 @@ export function parseWmicCsv(text: string): ProcessInfo[] {
|
|||||||
return processes;
|
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[] {
|
export function parsePs(text: string): ProcessInfo[] {
|
||||||
const processes: ProcessInfo[] = [];
|
const processes: ProcessInfo[] = [];
|
||||||
for (const line of text.split(/\r?\n/)) {
|
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) {
|
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;
|
return processes;
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ export class HotReloadSession implements vscode.Disposable {
|
|||||||
cwd: spec.cwd,
|
cwd: spec.cwd,
|
||||||
env,
|
env,
|
||||||
windowsHide: true,
|
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)));
|
this.child.stdout?.on('data', chunk => this.consume(String(chunk)));
|
||||||
|
|||||||
@@ -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';
|
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), [
|
assert.deepStrictEqual(parseWmicCsv(csv), [
|
||||||
{ pid: 100, parentPid: 1, name: 'dotnet.exe' }, { pid: 103, parentPid: 100, name: 'MyGame.Editor.exe' }]);
|
{ 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'), [
|
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' }]);
|
{ pid: 100, parentPid: 1, name: 'dotnet' }, { pid: 103, parentPid: 100, name: 'MyGame.Editor' }]);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user