103 lines · 4.1 KB · TypeScript
RawDownload
import { Effect } from "effect";import type { Accelerator, HandleReference, InstanceRecord, LaunchPlan } from "../contracts";import { runCommandAsyncEffect } from "../../../core/command";import { dockerFlagsFor } from "../engines/devices";import { LOG_TAIL_BYTES, spawnFailed, type Launcher } from "./launcher"; /** * Container launcher. Ownership is a label pair written at `docker run` time: the * instance name and the record's nonce. `owns` compares the nonce, so a container someone * recreated by hand under the same name is never signalled — the exact analogue of the * process launcher's start-token check. All state queries are one `docker inspect` by * exact name; nothing ever lists all containers and filters, which is what made the old * launch path O(running containers). */ const NAME_LABEL = "local-studio.instance";const NONCE_LABEL = "local-studio.nonce";const DOCKER_TIMEOUT_MS = 30_000; const containerName = (instanceName: string): string =>  `local-studio-${instanceName.replace(/[^a-zA-Z0-9_.-]/g, "_")}`; const docker = (  args: readonly string[],  timeoutMs = DOCKER_TIMEOUT_MS,): Effect.Effect<{ status: number | null; stdout: string; stderr: string }> =>  runCommandAsyncEffect("docker", [...args], { timeoutMs }); export const makeDockerLauncher = (accelerator: Accelerator): Launcher => ({  start: (plan: LaunchPlan, record: InstanceRecord) =>    Effect.gen(function* () {      if (!plan.image) return yield* spawnFailed(`no image for ${record.engine} on this host`);      const name = containerName(record.name);      // A previous container under our name that we no longer track is torn down first —      // by exact name, with force; `docker run` would otherwise fail on the name clash.      yield* docker(["rm", "-f", name]).pipe(Effect.ignore);      const deviceFlags = dockerFlagsFor(accelerator, plan.devices);      const arguments_: string[] = [        "run",        "-d",        "--name",        name,        "--label",        `${NAME_LABEL}=${record.name}`,        "--label",        `${NONCE_LABEL}=${record.nonce}`,        ...deviceFlags.args,        ...deviceFlags.groupAdd.flatMap((group) => ["--group-add", group]),        ...plan.ports.flatMap((binding) => ["-p", `${binding.host}:${binding.container}`]),        ...plan.mounts.flatMap((mount) => [          "-v",          `${mount.from}:${mount.to}${mount.readOnly ? ":ro" : ""}`,        ]),        ...Object.entries(plan.env).flatMap(([key, value]) => ["-e", `${key}=${value}`]),        plan.image,        ...plan.argv,      ];      const result = yield* docker(arguments_, 120_000);      if (result.status !== 0) {        return yield* spawnFailed(`docker run failed: ${result.stderr || result.stdout}`);      }      return { kind: "docker", container: name } as const;    }),   alive: (reference: HandleReference) =>    reference.kind !== "docker"      ? Effect.succeed(false)      : docker(["inspect", "-f", "{{.State.Running}}", reference.container]).pipe(          Effect.map((result) => result.status === 0 && result.stdout.trim() === "true"),        ),   owns: (reference: HandleReference, record: InstanceRecord) =>    reference.kind !== "docker"      ? Effect.succeed(false)      : docker([          "inspect",          "-f",          `{{index .Config.Labels "${NONCE_LABEL}"}}`,          reference.container,        ]).pipe(          Effect.map((result) => result.status === 0 && result.stdout.trim() === record.nonce),        ),   stop: (reference: HandleReference, graceMs: number) =>    reference.kind !== "docker"      ? Effect.void      : Effect.gen(function* () {          yield* docker(            ["stop", "-t", String(Math.ceil(graceMs / 1000)), reference.container],            graceMs + DOCKER_TIMEOUT_MS,          ).pipe(Effect.ignore);          yield* docker(["rm", "-f", reference.container]).pipe(Effect.ignore);        }),   logTail: (reference: HandleReference) =>    reference.kind !== "docker"      ? Effect.succeed("")      : docker(["logs", "--tail", "60", reference.container]).pipe(          Effect.map((result) =>            `${result.stdout}\n${result.stderr}`.trim().slice(-LOG_TAIL_BYTES),          ),        ),});