Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/cloud/bootService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ it("renders a systemd unit with absolute paths and append-mode logging", () => {
"Type=simple",
"WorkingDirectory=%h",
"Environment=T3CODE_HOME=/home/theo/.t3",
"Environment=T3_BOOT_SERVICE_UNIT=t3code.service",
"ExecStart=/usr/local/bin/node /home/theo/.t3/runtime/versions/0.0.27/node_modules/t3/dist/bin.mjs serve",
"Restart=always",
"RestartSec=5",
Expand Down
83 changes: 33 additions & 50 deletions apps/server/src/cloud/bootService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "@t3tools/shared/hostProcess";

import * as ProcessRunner from "../processRunner.ts";
import { ensurePinnedRuntimeInstalled, pinnedRuntimePaths } from "./pinnedRuntime.ts";

/**
* Installs T3 Code as a per-user boot service so a connected machine stays
Expand All @@ -27,10 +28,9 @@ import * as ProcessRunner from "../processRunner.ts";
*/

const BOOT_SERVICE_NAME = "t3code";
const BOOT_RUNTIME_DIR = "runtime";

const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;
const PINNED_RUNTIME_INSTALL_TIMEOUT = Duration.minutes(10);
export const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;
export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT";

const EPHEMERAL_CACHE_SEGMENTS = [
"/_npx/", // npx
Expand Down Expand Up @@ -103,6 +103,7 @@ export function renderBootServiceUnit(plan: BootServicePlan): string {
"Type=simple",
"WorkingDirectory=%h",
`Environment=T3CODE_HOME=${quoteSystemdValue(plan.baseDir)}`,
`Environment=${BOOT_SERVICE_UNIT_ENV}=${BOOT_SERVICE_UNIT_FILE}`,
`ExecStart=${quoteSystemdValue(plan.nodePath)} ${quoteSystemdValue(plan.t3EntryPath)} serve`,
"Restart=always",
"RestartSec=5",
Expand Down Expand Up @@ -206,14 +207,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
const unitDir = path.join(homeDir, ".config", "systemd", "user");
const unitPath = path.join(unitDir, BOOT_SERVICE_UNIT_FILE);
const logPath = path.join(input.logsDir, "boot-service.log");
const runtimeVersionDir = path.join(
input.baseDir,
BOOT_RUNTIME_DIR,
"versions",
input.cliVersion,
);
const runtimeEntryPath = path.join(runtimeVersionDir, "node_modules", "t3", "dist", "bin.mjs");
const runtimeSentinelPath = path.join(runtimeVersionDir, ".install-complete");
const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion);

const requireSystemdLinux = Effect.gen(function* () {
if (platform !== "linux" || homeDir === "") {
Expand Down Expand Up @@ -263,52 +257,41 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
if (!isEphemeralCacheEntry(host.cliEntryPath)) {
return;
}
// The sentinel is written only after npm exits 0. Checking the entry
// file alone is not enough: npm extracts files before running native
// builds (node-pty), so a killed install leaves a plausible-looking but
// broken tree behind.
const alreadyPinned = yield* Effect.all([
fs.exists(runtimeSentinelPath),
fs.exists(runtimeEntryPath),
]).pipe(
Effect.map(([sentinelExists, entryExists]) => sentinelExists && entryExists),
Effect.mapError((cause) => new BootServiceInstallError({ cause })),
);
if (alreadyPinned) {
return;
}
yield* fs.remove(runtimeVersionDir, { recursive: true, force: true }).pipe(
Effect.andThen(fs.makeDirectory(runtimeVersionDir, { recursive: true })),
Effect.mapError((cause) => new BootServiceInstallError({ cause })),
);
yield* runStep(
"installing the pinned t3 runtime (this can take a few minutes)",
"npm",
[
"install",
"--prefix",
runtimeVersionDir,
"--no-fund",
"--no-audit",
`t3@${input.cliVersion}`,
],
// Native deps (node-pty) can compile from source on slow boxes; the
// ProcessRunner default of 60s would kill a healthy install.
{ timeout: PINNED_RUNTIME_INSTALL_TIMEOUT },
).pipe(
Effect.tapError(() =>
fs.remove(runtimeVersionDir, { recursive: true, force: true }).pipe(Effect.ignore),
yield* ensurePinnedRuntimeInstalled({
baseDir: input.baseDir,
version: input.cliVersion,
fs,
path,
runner,
}).pipe(
Effect.mapError((error) =>
error.step.startsWith("installing")
? new BootServiceCommandError({
step: error.step,
exitCode: error.exitCode,
stdoutLength: error.stdoutLength,
stderrLength: error.stderrLength,
cause: error.cause,
})
: new BootServiceInstallError({ cause: error }),
),
Effect.tapError((error) =>
DateTime.now.pipe(
Effect.flatMap((now) =>
fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, {
flag: "a",
}),
),
Effect.ignore,
),
),
);
yield* fs
.writeFileString(runtimeSentinelPath, `${input.cliVersion}\n`)
.pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
});

// Where the unit will point: derivable without touching the network, so
// status can compare units purely; install materializes it first.
const plannedEntryPath = isEphemeralCacheEntry(host.cliEntryPath)
? runtimeEntryPath
? runtimePaths.entryPath
: host.cliEntryPath;
const plan: BootServicePlan = {
nodePath: host.execPath,
Expand Down
129 changes: 129 additions & 0 deletions apps/server/src/cloud/pinnedRuntime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";

import * as ProcessRunner from "../processRunner.ts";
import {
ensurePinnedRuntimeInstalled,
pinnedRuntimePaths,
removePinnedRuntimeInstallation,
} from "./pinnedRuntime.ts";

it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => {
it.effect("serializes concurrent installs of the same runtime", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-runtime-test-" });
const installStarted = yield* Deferred.make<void>();
const allowInstallToFinish = yield* Deferred.make<void>();
const paths = pinnedRuntimePaths(path, baseDir, "0.0.29");
let npmRuns = 0;

const runner = ProcessRunner.ProcessRunner.of({
run: (_input) =>
Effect.gen(function* () {
npmRuns += 1;
yield* Deferred.succeed(installStarted, undefined);
yield* Deferred.await(allowInstallToFinish);
yield* fs
.makeDirectory(path.dirname(paths.entryPath), { recursive: true })
.pipe(Effect.orDie);
yield* fs.writeFileString(paths.entryPath, "export {};\n").pipe(Effect.orDie);
return {
stdout: "",
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
};
}),
});
const install = ensurePinnedRuntimeInstalled({
baseDir,
version: "0.0.29",
fs,
path,
runner,
});

const first = yield* Effect.forkChild(install, { startImmediately: true });
yield* Deferred.await(installStarted);
const second = yield* Effect.forkChild(install, { startImmediately: true });
yield* Effect.yieldNow;
assert.equal(npmRuns, 1);

yield* Deferred.succeed(allowInstallToFinish, undefined);
yield* Fiber.join(first);
yield* Fiber.join(second);

assert.equal(npmRuns, 1);
assert.isTrue(yield* fs.exists(paths.sentinelPath));
assert.isTrue(yield* fs.exists(paths.entryPath));
}),
);

it.effect("waits for an active install before removing its runtime", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-runtime-test-" });
const installStarted = yield* Deferred.make<void>();
const allowInstallToFinish = yield* Deferred.make<void>();
const paths = pinnedRuntimePaths(path, baseDir, "0.0.30");
const runner = ProcessRunner.ProcessRunner.of({
run: (_input) =>
Effect.gen(function* () {
yield* Deferred.succeed(installStarted, undefined);
yield* Deferred.await(allowInstallToFinish);
yield* fs
.makeDirectory(path.dirname(paths.entryPath), { recursive: true })
.pipe(Effect.orDie);
yield* fs.writeFileString(paths.entryPath, "export {};\n").pipe(Effect.orDie);
return {
stdout: "",
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
};
}),
});

const installFiber = yield* Effect.forkChild(
ensurePinnedRuntimeInstalled({
baseDir,
version: "0.0.30",
fs,
path,
runner,
}),
{ startImmediately: true },
);
yield* Deferred.await(installStarted);
const removeFiber = yield* Effect.forkChild(
removePinnedRuntimeInstallation({
baseDir,
version: "0.0.30",
fs,
path,
}),
{ startImmediately: true },
);
yield* Effect.yieldNow;
assert.isTrue(yield* fs.exists(paths.versionDir));

yield* Deferred.succeed(allowInstallToFinish, undefined);
yield* Fiber.join(installFiber);
yield* Fiber.join(removeFiber);
assert.isFalse(yield* fs.exists(paths.versionDir));
}),
);
});
Loading
Loading