From f1fc4fcde8d19fc83eb782b0b20f6850bf726076 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 4 Mar 2026 19:09:30 -0800 Subject: [PATCH] Add error handling and timeout to system operations --- src/client/api.ts | 6 ++-- src/client/components/SettingsPage.tsx | 41 ++++++++++++++++---------- src/server/api/system.ts | 32 +++++++++++--------- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/client/api.ts b/src/client/api.ts index 9f8dcb5..9d76e13 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -14,15 +14,15 @@ export const unshareApp = (name: string) => fetch(`/api/apps/${name}/tunnel`, { method: 'DELETE' }) export const applyUpdate = () => - fetch('/api/system/update', { method: 'POST' }).then(r => r.json()) + fetch('/api/system/update', { method: 'POST' }).then(r => { if (!r.ok) throw new Error('update failed'); return r.json() }) export const checkForUpdate = (): Promise<{ available: boolean, current: string, latest: string, commits: string[] }> => - fetch('/api/system/update').then(r => r.json()) + fetch('/api/system/update').then(r => { if (!r.ok) throw new Error('check failed'); return r.json() }) export const restartApp = (name: string) => fetch(`/api/apps/${name}/restart`, { method: 'POST' }) export const restartServer = () => - fetch('/api/system/restart', { method: 'POST' }).then(r => r.json()) + fetch('/api/system/restart', { method: 'POST' }).then(r => { if (!r.ok) throw new Error('restart failed'); return r.json() }) export const startApp = (name: string) => fetch(`/api/apps/${name}/start`, { method: 'POST' }) diff --git a/src/client/components/SettingsPage.tsx b/src/client/components/SettingsPage.tsx index 84bee13..f115b7a 100644 --- a/src/client/components/SettingsPage.tsx +++ b/src/client/components/SettingsPage.tsx @@ -19,8 +19,15 @@ import { type UpdateInfo = { available: boolean, current: string, latest: string, commits: string[] } -function pollUntilBack(onBack: () => void) { +function pollUntilBack(onBack: () => void, onTimeout?: () => void) { + let elapsed = 0 const poll = setInterval(async () => { + elapsed += 2000 + if (elapsed > 60000) { + clearInterval(poll) + onTimeout?.() + return + } try { const res = await fetch('/api/system/info') if (res.ok) { @@ -62,17 +69,21 @@ export function SettingsPage({ render }: { render: () => void }) { setTheme() } + const refreshSystemInfo = () => { + getSystemInfo().then(info => { + setVersion(info.version) + setSha(info.sha) + }) + } + const handleRestart = () => { if (!confirm('Are you sure you want to restart the server?')) return setRestarting(true) restartServer().catch(() => {}) - pollUntilBack(() => { - setRestarting(false) - getSystemInfo().then(info => { - setVersion(info.version) - setSha(info.sha) - }) - }) + pollUntilBack( + () => { setRestarting(false); refreshSystemInfo() }, + () => { setRestarting(false) }, + ) } const handleCheckUpdate = async () => { @@ -91,15 +102,13 @@ export function SettingsPage({ render }: { render: () => void }) { setUpdating(true) try { await applyUpdate() - } catch {} - pollUntilBack(() => { + pollUntilBack( + () => { setUpdating(false); refreshSystemInfo(); setUpdateInfo(null) }, + () => { setUpdating(false) }, + ) + } catch { setUpdating(false) - getSystemInfo().then(info => { - setVersion(info.version) - setSha(info.sha) - }) - setUpdateInfo(null) - }) + } } return ( diff --git a/src/server/api/system.ts b/src/server/api/system.ts index 251deb8..b6da71d 100644 --- a/src/server/api/system.ts +++ b/src/server/api/system.ts @@ -186,8 +186,10 @@ router.sse('/metrics/stream', (send) => { }) // System info -const pkg = JSON.parse(readFileSync(join(import.meta.dir, '../../../package.json'), 'utf-8')) -const sha = Bun.spawnSync(['git', 'rev-parse', '--short', 'HEAD'], { cwd: join(import.meta.dir, '../../..') }).stdout.toString().trim() || 'unknown' +const projectRoot = join(import.meta.dir, '../../..') +const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8')) +const sha = Bun.spawnSync(['git', 'rev-parse', '--short', 'HEAD'], { cwd: projectRoot }).stdout.toString().trim() || 'unknown' +let isUpdating = false router.get('/info', c => { return c.json({ version: pkg.version, sha }) @@ -274,16 +276,15 @@ router.post('/restart', c => { // Check for updates router.get('/update', async c => { - const cwd = join(import.meta.dir, '../../..') try { - const fetch = Bun.spawnSync(['git', 'fetch', 'origin', 'main'], { cwd }) - if (fetch.exitCode !== 0) return c.json({ available: false }) + const gitFetch = await Bun.spawn(['git', 'fetch', 'origin', 'main'], { cwd: projectRoot }).exited + if (gitFetch !== 0) return c.json({ available: false }) - const log = Bun.spawnSync(['git', 'log', 'HEAD..origin/main', '--oneline'], { cwd }) + const log = Bun.spawnSync(['git', 'log', 'HEAD..origin/main', '--oneline'], { cwd: projectRoot }) const output = log.stdout.toString().trim() const commits = output ? output.split('\n') : [] - const latest = Bun.spawnSync(['git', 'rev-parse', '--short', 'origin/main'], { cwd }) + const latest = Bun.spawnSync(['git', 'rev-parse', '--short', 'origin/main'], { cwd: projectRoot }) .stdout.toString().trim() return c.json({ @@ -299,21 +300,24 @@ router.get('/update', async c => { // Apply update and restart router.post('/update', async c => { - const cwd = join(import.meta.dir, '../../..') + if (isUpdating) return c.json({ ok: false, error: 'update already in progress' }, 409) + isUpdating = true try { - const pull = Bun.spawnSync(['git', 'pull', 'origin', 'main'], { cwd }) - if (pull.exitCode !== 0) return c.json({ ok: false, error: 'git pull failed' }, 500) + const pull = await Bun.spawn(['git', 'pull', 'origin', 'main'], { cwd: projectRoot }).exited + if (pull !== 0) return c.json({ ok: false, error: 'git pull failed' }, 500) - const install = Bun.spawnSync(['bun', 'install'], { cwd }) - if (install.exitCode !== 0) return c.json({ ok: false, error: 'bun install failed' }, 500) + const install = await Bun.spawn(['bun', 'install'], { cwd: projectRoot }).exited + if (install !== 0) return c.json({ ok: false, error: 'bun install failed' }, 500) - const build = Bun.spawnSync(['bun', 'run', 'build'], { cwd }) - if (build.exitCode !== 0) return c.json({ ok: false, error: 'build failed' }, 500) + const build = await Bun.spawn(['bun', 'run', 'build'], { cwd: projectRoot }).exited + if (build !== 0) return c.json({ ok: false, error: 'build failed' }, 500) setTimeout(() => process.exit(0), 100) return c.json({ ok: true }) } catch { return c.json({ ok: false, error: 'update failed' }, 500) + } finally { + isUpdating = false } })