From 7f695eb9eb698c4c6579ce33740406df47d9bed2 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:57:22 -0700 Subject: [PATCH] comments, dns --- app/src/commands.ts | 3 +++ app/src/dispatch.ts | 35 ++++++++++++++++++----------- app/src/dns.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++ app/src/server.tsx | 8 +++++-- app/src/session.ts | 3 +++ app/src/shell.ts | 3 ++- app/src/sneaker.ts | 4 ++++ app/src/utils.tsx | 3 +++ app/src/webapp.ts | 54 +++++--------------------------------------- 9 files changed, 103 insertions(+), 65 deletions(-) create mode 100644 app/src/dns.ts diff --git a/app/src/commands.ts b/app/src/commands.ts index d783737..3a1e76f 100644 --- a/app/src/commands.ts +++ b/app/src/commands.ts @@ -1,3 +1,6 @@ +//// +// Manages the commands on disk, in NOSE_SYS_BIN and NOSE_BIN + import { Glob } from "bun" import { watch } from "fs" import { sendAll } from "./websocket" diff --git a/app/src/dispatch.ts b/app/src/dispatch.ts index bedfb86..13da7e0 100644 --- a/app/src/dispatch.ts +++ b/app/src/dispatch.ts @@ -1,24 +1,33 @@ //// -// Dispatch Messages +// Dispatch Messages received via WebSocket import { basename } from "path" import type { Message } from "./shared/types" import { runCommand } from "./shell" import { send } from "./websocket" -import { isFile } from "./utils" export async function dispatchMessage(ws: any, msg: Message) { - if (msg.type === "input") { - const result = await runCommand(msg.session || "", msg.id || "", msg.data as string) - send(ws, { id: msg.id, type: "output", data: result }) + switch (msg.type) { + case "input": + await inputMessage(ws, msg); break - } else if (msg.type === "save-file") { - if (msg.id && typeof msg.data === "string") { - await Bun.write(msg.id.replace("..", ""), msg.data, { createPath: true }) - send(ws, { type: "output", data: { status: "ok", output: `saved ${basename(msg.id)}` } }) - } + case "save-file": + await saveFileMessage(ws, msg); break - } else { - send(ws, { type: "error", data: `unknown message: ${msg.type}` }) + default: + send(ws, { type: "error", data: `unknown message: ${msg.type}` }) } -} \ No newline at end of file +} + +async function inputMessage(ws: any, msg: Message) { + const result = await runCommand(msg.session || "", msg.id || "", msg.data as string) + send(ws, { id: msg.id, type: "output", data: result }) +} + +async function saveFileMessage(ws: any, msg: Message) { + if (msg.id && typeof msg.data === "string") { + await Bun.write(msg.id.replace("..", ""), msg.data, { createPath: true }) + send(ws, { type: "output", data: { status: "ok", output: `saved ${basename(msg.id)}` } }) + } +} + diff --git a/app/src/dns.ts b/app/src/dns.ts new file mode 100644 index 0000000..0e8985e --- /dev/null +++ b/app/src/dns.ts @@ -0,0 +1,55 @@ +//// +// Publishes webapps as subdomains on your local network + +import { watch } from "fs" +import { apps } from "./webapp" +import { expectDir } from "./utils" +import { NOSE_WWW } from "./config" + +export const dnsEntries: Record = {} + +const { stdout: ipRaw } = await Bun.$`hostname -I | awk '{print $1}'`.quiet() +const { stdout: hostRaw } = await Bun.$`hostname`.quiet() + +const ip = ipRaw.toString().trim() +const host = hostRaw.toString().trim() +let dnsInit = false + +export async function initDNS() { + apps().forEach(publishAppDNS) + + const signals = ["SIGINT", "SIGTERM"] + signals.forEach(sig => + process.on(sig, () => { + for (const name in dnsEntries) + dnsEntries[name].kill("SIGTERM") + process.exit(0) + }) + ) + + dnsInit = true +} + +export function publishAppDNS(app: string) { + if (!dnsInit) throw "publishAppDNS() must be called after initDNS()" + if (process.env.NODE_ENV !== "production") return + + + if (!dnsEntries[app]) + dnsEntries[app] = Bun.spawn(["avahi-publish", "-a", `${app}.${host}.local`, "-R", ip]) + + return dnsEntries[app] +} + +// exit process with error if no WWW dir +expectDir(NOSE_WWW) + +const wwwWatcher = watch(NOSE_WWW, (event, filename) => { + const www = apps() + www.forEach(publishAppDNS) + for (const name in dnsEntries) + if (!www.includes(name)) { + dnsEntries[name].kill("SIGTERM") + delete dnsEntries[name] + } +}) \ No newline at end of file diff --git a/app/src/server.tsx b/app/src/server.tsx index a4ae9f3..f1061ba 100644 --- a/app/src/server.tsx +++ b/app/src/server.tsx @@ -1,3 +1,6 @@ +//// +// Web server that serves shell commands, websocket connections, etc + import { Hono } from "hono" import { serveStatic, upgradeWebSocket, websocket } from "hono/bun" import { prettyJSON } from "hono/pretty-json" @@ -6,7 +9,8 @@ import color from "kleur" import type { Message } from "./shared/types" import { NOSE_ICON, NOSE_BIN, NOSE_WWW } from "./config" import { transpile, isFile, tilde } from "./utils" -import { apps, serveApp, publishDNS } from "./webapp" +import { apps, serveApp } from "./webapp" +import { initDNS } from "./dns" import { commands } from "./commands" import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket" @@ -136,7 +140,7 @@ if (process.env.BUN_HOT) { }) } } else { - publishDNS() + initDNS() } diff --git a/app/src/session.ts b/app/src/session.ts index 52f41d5..023f616 100644 --- a/app/src/session.ts +++ b/app/src/session.ts @@ -1,3 +1,6 @@ +//// +// Session storage. 1 browser tab = 1 session + import { AsyncLocalStorage } from "async_hooks" export type Session = { diff --git a/app/src/shell.ts b/app/src/shell.ts index a92f2ea..aba5e05 100644 --- a/app/src/shell.ts +++ b/app/src/shell.ts @@ -1,5 +1,6 @@ //// -// runs commands and such. +// Runs commands and such on the server. +// This is the "shell" - the "terminal" is the browser UI. import { join } from "path" import type { CommandResult, CommandOutput } from "./shared/types" diff --git a/app/src/sneaker.ts b/app/src/sneaker.ts index 223850d..c617425 100644 --- a/app/src/sneaker.ts +++ b/app/src/sneaker.ts @@ -1,3 +1,7 @@ +//// +// Sneaker is our tunneling service that allows you to share your local NOSE webapps +// with the public internet. It requires a sneaker server, usually hosted by us. + import nose from "./server" const SNEAKER_URL = "nose.space" diff --git a/app/src/utils.tsx b/app/src/utils.tsx index a4ae6d1..9eb15df 100644 --- a/app/src/utils.tsx +++ b/app/src/utils.tsx @@ -1,3 +1,6 @@ +//// +// Shell utilities and helper functions. + import { Hono } from "hono" import { statSync } from "fs" import { basename } from "path" diff --git a/app/src/webapp.ts b/app/src/webapp.ts index c3e0310..2a9aa49 100644 --- a/app/src/webapp.ts +++ b/app/src/webapp.ts @@ -1,11 +1,14 @@ +//// +// Hosting for your NOSE webapps! + import type { Child } from "hono/jsx" import { type Context, Hono } from "hono" import { renderToString } from "hono/jsx/dom/server" import { join, dirname } from "path" -import { readdirSync, watch } from "fs" +import { readdirSync } from "fs" import { NOSE_WWW } from "./config" -import { expectDir, isFile } from "./utils" +import { isFile } from "./utils" export type Handler = (r: Context) => string | Child | Response | Promise export type App = Hono | Handler @@ -84,50 +87,3 @@ export function toResponse(source: string | Child | Response): Response { } }) } - -// -// dns nonsense -// - -const dnsEntries: Record = {} - -const { stdout: ipRaw } = await Bun.$`hostname -I | awk '{print $1}'`.quiet() -const { stdout: hostRaw } = await Bun.$`hostname`.quiet() - -const ip = ipRaw.toString().trim() -const host = hostRaw.toString().trim() - -export async function publishDNS() { - apps().forEach(publishAppDNS) - - const signals = ["SIGINT", "SIGTERM"] - signals.forEach(sig => - process.on(sig, () => { - for (const name in dnsEntries) - dnsEntries[name].kill("SIGTERM") - process.exit(0) - }) - ) -} - -function publishAppDNS(app: string) { - if (process.env.NODE_ENV !== "production") return - - if (!dnsEntries[app]) - dnsEntries[app] = Bun.spawn(["avahi-publish", "-a", `${app}.${host}.local`, "-R", ip]) - - return dnsEntries[app] -} - -// exit process with error if no WWW dir -expectDir(NOSE_WWW) - -const wwwWatcher = watch(NOSE_WWW, (event, filename) => { - const www = apps() - www.forEach(publishAppDNS) - for (const name in dnsEntries) - if (!www.includes(name)) { - dnsEntries[name].kill("SIGTERM") - delete dnsEntries[name] - } -}) \ No newline at end of file