startApp

tcmts.studio.startApp · #3ci2adgs · term · runs on the server

uses node:net

Start app name as tcm dev <name> does (it's run, on a free port, on the branch the studio serves), its output kept in .tcm/studio/logs/<name>.log; it runs on after the studio (Stop, or tcm ps, ends it). Where it's listening, once it is (it says so in .tcm/run), or that it's still starting.

import { liveRunning } from "#codebase/tcmts/cli/running";
import { HttpError } from "#codebase/tcmts/server";
import { editableCodebase, studioApps, studioCodebase, tcmEnv } from "#codebase/tcmts/studio";
import { spawn } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
import path from "node:path";

export async function startApp(name: string): Promise<string> {
  const cb = editableCodebase();
  const { cache } = studioCodebase();
  if (!studioApps(cb, cache.placement(cb)).some((a) => a.path === name))
    throw new HttpError(
      404,
      `${name} isn't an app: tcm dev runs a browser entry point nothing calls.`,
    );
  const root = cb.lay.root;
  if (liveRunning(root).some((r) => r.app === name && r.command === "dev"))
    throw new HttpError(409, `${name} is running already.`);
  const port = await new Promise<number>((resolve, reject) => {
    const s = net.createServer();
    s.once("error", reject);
    s.listen(0, "127.0.0.1", () => {
      const p = (s.address() as net.AddressInfo).port;
      s.close(() => resolve(p));
    });
  });
  const logs = path.join(cb.lay.tcmDir, "studio", "logs");
  fs.mkdirSync(logs, { recursive: true });
  const log = fs.openSync(path.join(logs, `${name}.log`), "w");
  const env = tcmEnv();
  const child = spawn(process.execPath, [process.argv[1]!, "dev", name, "--port", String(port)], {
    cwd: root,
    env,
    detached: true,
    stdio: ["ignore", log, log],
  });
  child.unref();
  fs.closeSync(log);
  for (let i = 0; i < 120; i++) {
    const r = liveRunning(root).find((x) => x.pid === child.pid);
    if (r?.url) return r.url;
    if (child.exitCode !== null) {
      const said = fs
        .readFileSync(path.join(logs, `${name}.log`), "utf8")
        .trim()
        .split("\n")
        .slice(-8)
        .join("\n");
      throw new HttpError(400, said || `tcm dev ${name} ended`);
    }
    await new Promise((r) => setTimeout(r, 500));
  }
  return `still starting: .tcm/studio/logs/${name}.log says how it's going`;
}

uses

used by