try to show error page on fatal errors

This commit is contained in:
Chris Wanstrath 2025-10-01 11:22:28 -07:00
parent 3ac1ba4f23
commit 14645980af
7 changed files with 100 additions and 14 deletions

View File

@ -56,7 +56,7 @@ export async function loadCommandModule(cmd: string) {
let sysCmdWatcher
let usrCmdWatcher
function startWatchers() {
expectDir(NOSE_BIN)
if (!expectDir(NOSE_BIN)) return
sysCmdWatcher = watch(NOSE_SYS_BIN, async (event, filename) =>
sendAll({ type: "commands", data: await commands() })

View File

@ -44,7 +44,7 @@ export function publishAppDNS(app: string) {
let wwwWatcher
function startWatcher() {
expectDir(NOSE_WWW)
if (!expectDir(NOSE_WWW)) return
wwwWatcher = watch(NOSE_WWW, (event, filename) => {
const www = apps()

10
src/fatal.ts Normal file
View File

@ -0,0 +1,10 @@
////
// We want to show a blue screen of death if NOSE has a fatal error.
export let fatal: string | undefined
export function setFatal(error: string) {
console.error(error)
if (!fatal)
fatal = error
}

60
src/html/error.tsx Normal file
View File

@ -0,0 +1,60 @@
import type { FC } from "hono/jsx"
import { css, js } from "../helpers"
export const Error: FC = async ({ error }) => (
<>
{css`
:root {
--letterbox: #CC8800;
--text: #FFBF40;
--bg: #201600;
}
* { color: var(--text); text-align: center; }
html { background: var(--letterbox); }
#content { background-color: var(--bg); }
h1 { max-width: 38%; margin: 0 auto; margin-top: 100px; }
h2 { max-width: 80%; margin: 0 auto; margin-top: 10px; }
p { max-width: 90%; margin: 0 auto; margin-top: 150px; }
.restart { max-width: 35%; }
.restart button {
font-size: 30px;
background: var(--letterbox);
color: var(--red);
}
h1 { animation: glow 3s ease-in-out infinite alternate; }
@keyframes glow {
0% {
text-shadow: none;
}
100% {
text-shadow:
0 0 2px #FFAA33,
0 0 4px #FF8800,
0 0 6px #FF6600;
}
}
`}
{js`
window.addEventListener("click", async e => {
if (!e.target || !e.target.matches("button")) return
e.target.textContent = "[RESTARTING...]"
await fetch("/cmd/restart")
setTimeout(() => window.location.reload(), 3000)
})
`}
<h1>Fatal Error</h1>
<h2>NOSE failed to start properly.</h2>
<br />
<p>{error}</p>
<p class="restart">
<button>[RESTART]</button>
</p>
</>
)

View File

@ -3,12 +3,13 @@
import { $ } from "bun"
import { NOSE_DIR } from "./config"
import { isDir } from "./utils"
import { expectDir } from "./utils"
// Make NOSE_DIR if it doesn't exist
export async function initNoseDir() {
if (isDir(NOSE_DIR)) return
if (expectDir(NOSE_DIR)) return
await $`cp -r ./nose ${NOSE_DIR}`
expectDir(NOSE_DIR)
}

View File

@ -8,17 +8,20 @@ import color from "kleur"
import type { Message } from "./shared/types"
import { NOSE_ICON, NOSE_BIN, NOSE_WWW, NOSE_DATA, NOSE_DIR } from "./config"
import { transpile, isFile, tilde } from "./utils"
import { transpile, isFile, tilde, isDir } from "./utils"
import { serveApp } from "./webapp"
import { initDNS } from "./dns"
import { commands, commandPath, loadCommandModule } from "./commands"
import { runCommandFn } from "./shell"
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
import { initSneakers, disconnectSneakers } from "./sneaker"
import { dispatchMessage } from "./dispatch"
import { fatal } from "./fatal"
import { Layout } from "./html/layout"
import { Terminal } from "./html/terminal"
import { dispatchMessage } from "./dispatch"
import { initSneakers, disconnectSneakers } from "./sneaker"
import { Error } from "./html/error"
import { initDNS } from "./dns"
import { initNoseDir } from "./nosedir"
import { initCommands } from "./commands"
@ -43,6 +46,16 @@ app.use("*", async (c, next) => {
console.log(fn(`${c.res.status} ${c.req.method} ${c.req.url} (${end - start}ms)`))
})
app.use("*", async (c, next) => {
const error = fatal ? fatal : !isDir(NOSE_DIR) ? `NOSE_DIR doesn't exist: ${NOSE_DIR}` : undefined
if (!error || (["/cmd/restart", "/cmd/reboot"].includes(c.req.path))) {
await next()
} else {
return c.html(<Layout><Error error={error} /></Layout>, 500)
}
})
app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => {
let path = "./src/" + c.req.path.replace("..", ".")

View File

@ -2,7 +2,7 @@
// Shell utilities and helper functions.
import { statSync } from "fs"
import { basename } from "path"
import { setFatal } from "./fatal"
import { stat } from "fs/promises"
// Convert /Users/$USER or /home/$USER to ~ for simplicity
@ -17,11 +17,13 @@ export function untilde(path: string): string {
}
// End the process with an instructive error if a directory doesn't exist.
export function expectDir(path: string) {
export function expectDir(path: string): boolean {
if (!isDir(path)) {
console.error(`No ${basename(path)} directory detected.`)
process.exit(1)
setFatal(`Missing critical directory: ${path}`)
return false
}
return true
}
// Is the given `path` a file?