resilience

This commit is contained in:
Chris Wanstrath 2025-10-10 16:12:06 -07:00
parent 9c2550150b
commit e3fa95ab68
3 changed files with 28 additions and 10 deletions

View File

@ -9,7 +9,7 @@ import type { Message } from "./shared/types"
import { rewriteJsImports } from "./build" import { rewriteJsImports } from "./build"
import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, BUN_BIN, DEFAULT_PROJECT } from "./config" import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, BUN_BIN, DEFAULT_PROJECT } from "./config"
import { transpile, isFile, tilde, isDir } from "./utils" import { transpile, isFile, tilde, isDir } from "./utils"
import { serveApp, initWebapps } from "./webapp/server" import { serveApp, initWebapps, shutdownWebapps } from "./webapp/server"
import { apps } from "./webapp/utils" import { apps } from "./webapp/utils"
import { commands, commandPath, loadCommandModule } from "./commands" import { commands, commandPath, loadCommandModule } from "./commands"
import { runCommandFn } from "./shell" import { runCommandFn } from "./shell"
@ -217,6 +217,7 @@ if (process.env.BUN_HOT) {
globalThis.__hot_reload_cleanup = () => { globalThis.__hot_reload_cleanup = () => {
closeWebsockets() closeWebsockets()
disconnectSneakers() disconnectSneakers()
shutdownWebapps()
} }
for (const sig of ["SIGINT", "SIGTERM"] as const) { for (const sig of ["SIGINT", "SIGTERM"] as const) {

View File

@ -16,10 +16,14 @@ export type App = Hono | Handler
const processes = new Map<string, { port: string, proc: ReturnType<typeof Bun.spawn> }>() const processes = new Map<string, { port: string, proc: ReturnType<typeof Bun.spawn> }>()
const restarting = new Set<string>() const restarting = new Set<string>()
let nextPort = 4000
export async function initWebapps() { export async function initWebapps() {
await startSubprocs() await startSubprocs()
startWatcher() startWatcher()
process.on("SIGINT", shutdownWebapps)
process.on("SIGTERM", shutdownWebapps)
} }
export async function serveApp(c: Context, subdomain: string): Promise<Response> { export async function serveApp(c: Context, subdomain: string): Promise<Response> {
@ -52,7 +56,7 @@ async function startApp(name: string): Promise<string | undefined> {
const existing = processes.get(name) const existing = processes.get(name)
if (existing) return existing.port if (existing) return existing.port
const port = String(4000 + processes.size) const port = String(nextPort++)
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd: [BUN_BIN, "run", "src/webapp/worker.ts", name], cmd: [BUN_BIN, "run", "src/webapp/worker.ts", name],
env: { PORT: port }, env: { PORT: port },
@ -78,11 +82,16 @@ function serveStatic(path: string): Response {
}) })
} }
async function shutdown() { export async function shutdownWebapps() {
wwwWatcher?.close()
nextPort = 4000
for (const [name, { port, proc }] of processes) { for (const [name, { port, proc }] of processes) {
webappLog(name, "Shutting down") webappLog(name, "Shutting down")
try { proc.kill() } catch { } try { proc.kill() } catch { }
} }
processes.clear()
process.exit(0) process.exit(0)
} }
@ -90,6 +99,8 @@ async function restartApp(name: string) {
if (restarting.has(name)) return if (restarting.has(name)) return
restarting.add(name) restarting.add(name)
if (isStaticApp(name)) return
webappLog(name, "restarting") webappLog(name, "restarting")
const existing = processes.get(name) const existing = processes.get(name)
if (existing) { if (existing) {
@ -108,7 +119,7 @@ async function startSubprocs() {
} }
let wwwWatcher let wwwWatcher: any
function startWatcher() { function startWatcher() {
if (!expectDir(NOSE_DIR)) return if (!expectDir(NOSE_DIR)) return
@ -116,13 +127,10 @@ function startWatcher() {
if (!filename) return if (!filename) return
const [appName,] = filename.split("/") const [appName,] = filename.split("/")
if (appName && !filename.includes("/pub/")) if (appName && /\.tsx?$/.test(filename))
restartApp(appName) restartApp(appName)
if (/^.+\/index\.tsx?$/.test(filename)) if (/^.+\/index\.tsx?$/.test(filename))
sendAll({ type: "apps", data: apps() }) sendAll({ type: "apps", data: apps() })
}) })
} }
process.on("SIGINT", shutdown)
process.on("SIGTERM", shutdown)

View File

@ -13,10 +13,19 @@ if (!appName) {
const path = await appPath(appName) const path = await appPath(appName)
if (!path) throw `Can't find app: ${appName}` if (!path) throw `Can't find app: ${appName}`
const mod = await import(path) let mod
try {
mod = await import(path)
} catch (err) {
webappLog(appName, `failed to import: ${err}`)
process.exit(1)
}
const handler = mod.default const handler = mod.default
if (typeof handler !== "function") throw `no default export in ${appName}` if (typeof handler !== "function") {
webappLog(appName, `no default export`)
process.exit(1)
}
const app = new Hono() const app = new Hono()
app.all("*", async c => toResponse(await handler(c))) app.all("*", async c => toResponse(await handler(c)))