webUi

tcmts.cli.sync.webUi · #h6q63l3k · term · runs on the server

uses node:path

The web UI tcm serve shows under /forge/: the app tcm ships (share), run by tcm start beside the server on the codebase it serves, started when it's first asked for (or ahead: warm). Its codebase is copied into the served one's .tcm/share/ (again when tcm's changes), where its builds are kept; requests under /forge/ go to it (a page asked for while it starts says so, and tries again by itself). It's told the server's token and whether it's public, and signs people in by the same tokens.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { SHIPPED_APP } from "#codebase/tcmts/cli/pack";
import { dim, yellow } from "#codebase/tcmts/cli/style";
import { forward, freePort, shippedApp, uiOutput } from "#codebase/tcmts/cli/sync";
import { BASE } from "#codebase/tcmts/share";
import { spawn } from "node:child_process";
import type { ChildProcess } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import type http from "node:http";
import path from "node:path";

export function webUi(cb: Codebase, o: { token: string; isPublic: boolean }) {
  let child: ChildProcess | undefined;
  let port: Promise<number> | undefined;
  const answered = { at: 0 };
  const start = (): Promise<number> | undefined => {
    const shipped = shippedApp("share");
    if (!shipped) return undefined;
    const dir = path.join(cb.lay.tcmDir, "share");
    const from = path.join(shipped, "codebase.sqlite");
    const to = path.join(dir, ".tcm", "codebase.sqlite");
    const digest = createHash("sha256").update(fs.readFileSync(from)).digest("hex");
    const mark = path.join(dir, ".tcm", "shipped");
    if (!fs.existsSync(to) || !fs.existsSync(mark) || fs.readFileSync(mark, "utf8") !== digest) {
      fs.mkdirSync(path.dirname(to), { recursive: true });
      for (const f of [to, `${to}-wal`, `${to}-shm`]) fs.rmSync(f, { force: true });
      fs.copyFileSync(from, to);
      fs.writeFileSync(mark, digest);
    }
    return freePort().then((p) => {
      const env: NodeJS.ProcessEnv = {
        ...process.env,
        TCM_SHARE_CODEBASE: cb.lay.root,
        TCM_SHARE_TOKEN: o.token,
        TCM_SHARE_PUBLIC: o.isPublic ? "1" : "0",
        // (it stops when this server does, however this one ends)
        TCM_STOP_WITH_PARENT: "1",
      };
      delete env.TCM_CODEBASE;
      delete env.TCM_BRANCH;
      const c = spawn(
        process.execPath,
        [process.argv[1]!, "start", SHIPPED_APP, "--port", String(p), "--base", BASE],
        { cwd: dir, env, stdio: ["ignore", "pipe", "pipe"] },
      );
      child = c;
      const sayer = () => {
        const shown = uiOutput();
        return (d: Buffer) => {
          for (const line of shown(d.toString("utf8"))) console.log(dim(`  ui  ${line}`));
        };
      };
      c.stdout?.on("data", sayer());
      c.stderr?.on("data", sayer());
      c.on("exit", (code) => {
        if (child === c) ((child = undefined), (port = undefined));
        if (code)
          console.log(yellow(`  ui  stopped (${code}); it starts again when it's next asked for`));
      });
      const stop = () => c.kill();
      process.once("exit", stop);
      return p;
    });
  };
  return {
    handle(req: http.IncomingMessage, res: http.ServerResponse) {
      port ??= start();
      if (!port) {
        res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
        return res.end("This tcm doesn't ship the web UI: a tcm installed from its package does.");
      }
      void port.then(
        (p) => forward(req, res, `http://127.0.0.1:${p}`, answered),
        (e: Error) => {
          port = undefined;
          res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
          res.end(`the web UI couldn't start: ${e.message}`);
        },
      );
    },
    /** starts it now, before anyone asks for it: a public server's first reader doesn't wait for its first build */
    warm() {
      const p = (port ??= start());
      p?.catch(() => {
        if (port === p) port = undefined;
      });
    },
    close() {
      child?.kill();
    },
  };
}

uses

used by