From 7935469776795ae5b149cc8f80fbe2d322887476 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 12 May 2026 18:29:05 -0700 Subject: [PATCH] Refactor app reload logic into dedicated function --- src/server/api/apps.ts | 5 ++-- src/server/api/sync.ts | 25 +++++------------ src/server/apps.ts | 63 ++++++++++++++++++++++++++++++++++++++++++ src/server/tunnels.ts | 3 ++ 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/server/api/apps.ts b/src/server/api/apps.ts index c777ce9..a94f289 100644 --- a/src/server/api/apps.ts +++ b/src/server/api/apps.ts @@ -196,12 +196,11 @@ router.post('/:app/start', c => { return c.json({ ok: true }) }) -router.post('/:app/restart', c => { +router.post('/:app/restart', async c => { const appName = c.req.param('app') if (!appName) return c.json({ error: 'App not found' }, 404) - stopApp(appName) - startApp(appName) + await restartApp(appName) return c.json({ ok: true }) }) diff --git a/src/server/api/sync.ts b/src/server/api/sync.ts index e82bf6b..d1475eb 100644 --- a/src/server/api/sync.ts +++ b/src/server/api/sync.ts @@ -1,4 +1,4 @@ -import { APPS_DIR, allApps, emit, registerApp, removeApp, restartApp, startApp } from '$apps' +import { APPS_DIR, allApps, emit, registerApp, reloadApp, removeApp, restartApp, startApp } from '$apps' import { computeHash, generateManifest } from '../sync' import { loadGitignore } from '@gitignore' import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, unlinkSync, watch, writeFileSync } from 'fs' @@ -115,25 +115,14 @@ router.post('/apps/:app/reload', async c => { const appName = c.req.param('app') if (!appName) return c.json({ error: 'App name required' }, 400) - emit({ type: 'app:reload', app: appName }) - - // Register new app or restart existing - const app = allApps().find(a => a.name === appName) - if (!app) { - // New app - register it - registerApp(appName) - } else if (app.state === 'running') { - // Existing app - restart it - try { - await restartApp(appName) - } catch (e) { - return c.json({ error: `Failed to restart app: ${e instanceof Error ? e.message : String(e)}` }, 500) - } - } else if (app.state === 'stopped' || app.state === 'invalid') { - // App not running - try to start it - startApp(appName) + try { + await reloadApp(appName) + } catch (e) { + return c.json({ error: `Failed to reload app: ${e instanceof Error ? e.message : String(e)}` }, 500) } + emit({ type: 'app:reload', app: appName }) + return c.json({ ok: true }) }) diff --git a/src/server/apps.ts b/src/server/apps.ts index bbeebb9..5566008 100644 --- a/src/server/apps.ts +++ b/src/server/apps.ts @@ -176,6 +176,69 @@ export function registerApp(dir: string) { } } +export async function reloadApp(dir: string) { + const app = _apps.get(dir) + if (!app) { + // New app — register it + registerApp(dir) + return + } + + // Re-read config from disk + const { pkg, error } = loadApp(dir) + const wasStatic = app.static + const nowStatic = !!pkg.toes?.static + + // Update cached metadata + app.icon = pkg.toes?.icon ?? DEFAULT_EMOJI + app.tool = pkg.toes?.tool + app.apps = pkg.toes?.apps + app.dashboard = pkg.toes?.dashboard + app.share = pkg.toes?.share + app.static = pkg.toes?.static + app.error = error + + if (error) { + // App is now invalid + if (app.state === 'running' || app.state === 'starting') { + stopApp(dir) + } + app.state = 'invalid' + update() + return + } + + if (nowStatic) { + // Stop process if transitioning from process-based to static + if (!wasStatic && (app.state === 'running' || app.state === 'starting')) { + stopApp(dir) + // Wait for stop + const maxWait = 10000 + const poll = 100 + let waited = 0 + while (_apps.get(dir)?.state !== 'stopped' && waited < maxWait) { + await new Promise(r => setTimeout(r, poll)) + waited += poll + } + } + // Mark as running (static apps are always "running") + app.state = 'running' + app.started = Date.now() + app.manuallyStopped = false + update() + publishApp(dir) + openTunnelIfEnabled(dir, SERVER_PORT) + return + } + + // Process-based app — restart it + if (app.state === 'running' || app.state === 'starting') { + await restartApp(dir) + } else { + startApp(dir) + } +} + export async function renameApp(oldName: string, newName: string): Promise<{ ok: boolean, error?: string }> { const app = _apps.get(oldName) if (!app) return { ok: false, error: 'App not found' } diff --git a/src/server/tunnels.ts b/src/server/tunnels.ts index 942a1dd..d88ebdc 100644 --- a/src/server/tunnels.ts +++ b/src/server/tunnels.ts @@ -180,6 +180,9 @@ function openTunnel(appName: string, port: number, subdomain?: string, isReconne onRequest(req) { const app = getApp(appName) if (app?.tunnelUrl) req.headers['x-app-url'] = app.tunnelUrl + // Static apps are served by the main server via subdomain routing, + // so set the Host header so extractSubdomain() can identify the app + if (app?.static) req.headers['host'] = `${appName}.localhost` }, onOpen(assignedSubdomain) {