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
37 changes: 37 additions & 0 deletions apps/desktop/e2e/quit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,40 @@ test('quitting DevHub stops the whole process tree of running scripts', async ({
await app.close()
await expect.poll(() => tree.filter(isAlive), { timeout: 15_000 }).toEqual([])
})

// Logout, shutdown and `systemctl --user stop` send SIGTERM first; Windows has no equivalent.
test.describe('SIGTERM', () => {
test.skip(process.platform === 'win32', 'POSIX signals only')

test('quits DevHub gracefully when nothing is running', async ({ launchDevhub }) => {
const { app } = await launchDevhub()
const child = app.process()
const exited = new Promise((resolve) => child.once('exit', resolve))

child.kill('SIGTERM')
await exited
// A graceful quit exits normally; the default handler would report the signal instead.
expect({ code: child.exitCode, signal: child.signalCode }).toEqual({ code: 0, signal: null })
})

test('stops the process tree of running scripts', async ({ launchDevhub, createProject }) => {
const dir = await createProject('server', {
'package.json': packageJson({ serve: 'node server.js' }),
'server.js': serverScript
})
const { app, page } = await launchDevhub()
await addProjectViaApi(page, dir)
await page.getByRole('button', { name: '运行 serve' }).click()
await expect(page.locator('.xterm-rows')).toContainText('serving')
const pids = pidsSchema.parse(JSON.parse(await readFile(join(dir, 'pids.json'), 'utf8')))
const tree = [pids.parent, pids.child]
expect(tree.every(isAlive)).toBe(true)

const child = app.process()
const exited = new Promise((resolve) => child.once('exit', resolve))
child.kill('SIGTERM')
await exited
expect(child.exitCode).toBe(0)
await expect.poll(() => tree.filter(isAlive), { timeout: 15_000 }).toEqual([])
})
})
6 changes: 5 additions & 1 deletion apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ if (!app.requestSingleInstanceLock()) {
const timeout = new Promise((resolve) => setTimeout(resolve, disposeTimeoutMs))
void Promise.race([core.dispose(), timeout])
.catch((error: unknown) => console.error('[quit] failed to stop runs', error))
.finally(() => app.quit())
// Not directly: with nothing to stop, dispose settles in the microtasks Node drains as
// this handler returns. On SIGTERM Electron calls Quit() with no JS on the stack, so a
// nested quit would run inside it and be overwritten by this prevented one, leaving
// DevHub running with every window closed (#39).
.finally(() => setImmediate(() => app.quit()))
})

// With "keep in tray" the window only hides, so this fires only for "quit on close".
Expand Down
2 changes: 1 addition & 1 deletion docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ M4-3 Android:
桌面修复:

- #22 已完成:启动的子进程不再直接继承 DevHub 的环境:Windows 从注册表修复并补全 PATH(更新后经 Explorer 重启时 `%NVM_HOME%` 未展开、找不到 pnpm),Linux 捕获交互登录 shell 的环境(从桌面启动时缺少 `.bashrc` 中的 nvm)(ADR 0024)。
- #39 待开始:Linux 上收到 SIGTERM 不退出(#8 中发现),关机或注销时可能留下孤儿进程。
- #39 已完成,待合并:Linux 上收到 SIGTERM 不退出(#8 中发现)。没有运行中的脚本时,`before-quit` 里的 `app.quit()` 会在 Electron 处理信号的 `Quit()` 内部重入,退出状态被外层覆盖,窗口关闭后进程挂住;改为推迟到下一轮事件循环。E2E 覆盖 SIGTERM 退出与停止进程树(Windows 跳过)。
- #42 已完成,待合并:远程服务启动时端口被占用(更新后旧进程尚未退出)会重试约 15 秒,期间状态显示「正在重试」;修改设置时的冲突仍立即报告。

桌面 MVP 发布:
Expand Down
Loading