Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions changes/3329-tailnet-watch-wrapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Fixed
---

## en

- The Host no longer hangs on shutdown or plugin reload when `tailscale` on PATH is a shell wrapper that starts the real binary as a child (as some macOS installs do): the tailnet watcher now runs in its own process group, stopping it also stops the real Tailscale process, and disposal no longer waits on a pipe a leftover process holds open ([#3329](https://github.com/BotHarness/DeepSeekBot/pull/3329)).

## zh

- 当 PATH 上的 `tailscale` 是一个以子进程方式启动真实程序的 shell 包装脚本时(部分 macOS 安装如此),Host 关闭或插件重载不再卡住:tailnet 监听进程改为在独立进程组中运行,停止时会一并结束真实的 Tailscale 进程,释放时也不再等待残留进程占用的管道([#3329](https://github.com/BotHarness/DeepSeekBot/pull/3329))。
39 changes: 32 additions & 7 deletions packages/core/src/devices/bot-tailnet-watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,39 @@ export function createTailnetNotificationReader(changed: (change: TailnetChange)
};
}

export function spawnOwnedProcess(file: string, args: readonly string[]): ChildProcess {
return spawn(file, [...args], {
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
detached: process.platform !== 'win32',
});
}

export function stopOwnedProcess(child: ChildProcess): void {
if (child.pid === undefined) return;
if (process.platform !== 'win32') {
try {
process.kill(-child.pid, 'SIGTERM');
return;
} catch {
child.kill();
return;
}
}
child.kill();
}

export function watchTailnet(
changed: (change: TailnetChange) => void,
stopped: (code: 'tailnet-watch-unavailable' | 'tailnet-watch-invalid') => void,
startProcess: (binary: string) => ChildProcess = (binary) =>
spawn(
binary,
['debug', 'watch-ipn', '--initial-status', '--peer-changes', '--peer-patches=false'],
{ stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true },
),
spawnOwnedProcess(binary, [
'debug',
'watch-ipn',
'--initial-status',
'--peer-changes',
'--peer-patches=false',
]),
): () => Promise<void> {
const binaries =
process.platform === 'darwin'
Expand All @@ -94,12 +118,13 @@ export function watchTailnet(
const fail = (code: 'tailnet-watch-unavailable' | 'tailnet-watch-invalid') => {
if (closed) return;
closed = true;
if (child?.pid !== undefined) child.kill();
if (child !== undefined) stopOwnedProcess(child);
void Promise.all(exits).then(() => stopped(code));
};
const launch = (index: number) => {
const process = startProcess(binaries[index]!);
exits.push(new Promise<void>((resolve) => process.once('close', () => resolve())));
process.once('exit', () => process.stdout?.destroy());
child = process;
const read = createTailnetNotificationReader(changed);
process.stdout?.setEncoding('utf8');
Expand All @@ -123,7 +148,7 @@ export function watchTailnet(
launch(0);
return async () => {
closed = true;
if (child?.pid !== undefined) child.kill();
if (child !== undefined) stopOwnedProcess(child);
await Promise.all(exits);
};
}
Loading
Loading