Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions apps/server/src/cli/servicePreflight.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,8 +10,8 @@ export const servicePreflightCommand = Command.make("__service-preflight", {
}).pipe(
Command.withHidden,
Command.withHandler(({ databasePath, launcherProtocol }) =>
Console.log(JSON.stringify(runServicePreflight({ databasePath, launcherProtocol }))).pipe(
Effect.asVoid,
Effect.promise(() => runServicePreflight({ databasePath, launcherProtocol })).pipe(
Effect.flatMap((result) => Console.log(JSON.stringify(result))),
),
),
);
9 changes: 9 additions & 0 deletions apps/server/src/cloud/pinnedRuntime.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,15 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => {
validatedDirectory = staging.versionDir;
assert.isFalse(yield* fs.exists(finalPaths.versionDir));
assert.isTrue(yield* fs.exists(staging.entryPath));
// The manifest npm read while installing must let node-pty build.
const manifest = yield* fs.readFileString(
path.join(staging.versionDir, "package.json"),
);
// @effect-diagnostics-next-line preferSchemaOverJson:off - staged file written by the code under test.
assert.deepEqual((JSON.parse(manifest) as { allowScripts: unknown }).allowScripts, {
"node-pty": true,
"msgpackr-extract": true,
});
}).pipe(Effect.orDie),
});

Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/cloud/pinnedRuntime.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,15 @@ import * as ProcessRunner from "../processRunner.ts";

const PINNED_RUNTIME_DIR = "runtime";
const PINNED_RUNTIME_INSTALL_TIMEOUT = Duration.minutes(10);
// npm 12 skips dependency install scripts not covered by allowScripts and
// still exits 0, silently dropping node-pty's native build. It rejects
// --allow-scripts for project installs (unlike the global installs in
// providerMaintenance.ts), so the staged manifest is the only place to grant this.
const PINNED_RUNTIME_PACKAGE_JSON = `${JSON.stringify({
private: true,
// Keep this native subset aligned with pnpm-workspace.yaml#allowBuilds.
allowScripts: { "node-pty": true, "msgpackr-extract": true },
})}\n`;
// Boot-service setup and remote update can construct separate layers. Serialize
// the complete install transaction across every caller in this process.
const pinnedRuntimeInstallLock = Semaphore.makeUnsafe(1);
Expand DownExpand Up@@ -151,6 +160,14 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")(
};

return yield* Effect.gen(function* () {
yield* fs
.writeFileString(input.path.join(stagingDir, "package.json"), PINNED_RUNTIME_PACKAGE_JSON)
.pipe(
Effect.mapError(
(cause) =>
new PinnedRuntimeInstallError({ step: "configuring native dependency builds", cause }),
),
);
const installStep = "installing the pinned t3 runtime (this can take a few minutes)";
yield* runner
.run({
Expand Down
20 changes: 15 additions & 5 deletions apps/server/src/cloud/servicePreflight.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,24 +3,34 @@ import { expect, it } from "@effect/vitest";
import { runServicePreflight } from "./servicePreflight.ts";
import { SERVICE_LAUNCHER_PROTOCOL } from "./serviceProtocol.ts";

it("requires the database-snapshot launcher protocol", () => {
expect(
it("requires the database-snapshot launcher protocol", async () => {
await expect(
runServicePreflight({
databasePath: "/missing/state.sqlite",
launcherProtocol: SERVICE_LAUNCHER_PROTOCOL - 1,
version: "1.2.3",
}),
).toMatchObject({ status: "blocked", version: "1.2.3" });
).resolves.toMatchObject({ status: "blocked", version: "1.2.3" });

expect(
await expect(
runServicePreflight({
databasePath: "/missing/state.sqlite",
launcherProtocol: SERVICE_LAUNCHER_PROTOCOL,
version: "1.2.3",
}),
).toEqual({
).resolves.toEqual({
status: "ready",
version: "1.2.3",
launcherProtocol: SERVICE_LAUNCHER_PROTOCOL,
});
});

it("blocks a runtime whose node-pty does not load", async () => {
const result = await runServicePreflight(
{ databasePath: "/missing/state.sqlite", launcherProtocol: SERVICE_LAUNCHER_PROTOCOL },
() => Promise.reject(new Error("Cannot find module 'pty.node'")),
);

expect(result).toMatchObject({ status: "blocked" });
expect(result.status === "blocked" && result.reason).toContain("Cannot find module 'pty.node'");
});
27 changes: 21 additions & 6 deletions apps/server/src/cloud/servicePreflight.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,17 @@ export type ServicePreflightResult =
readonly reason: string;
};

export function runServicePreflight(input: {
/** Older servers always pass this flag when invoking a staged preflight. */
readonly databasePath: string;
readonly launcherProtocol: number;
readonly version?: string;
}): ServicePreflightResult {
// Loading node-pty is part of the proof: npm can skip its native build and
// still exit 0, leaving a runtime that boots but cannot open terminals.
export async function runServicePreflight(
input: {
/** Older servers always pass this flag when invoking a staged preflight. */
readonly databasePath: string;
readonly launcherProtocol: number;
readonly version?: string;
},
loadNodePty: () => Promise<unknown> = () => import("node-pty"),
): Promise<ServicePreflightResult> {
const version = input.version ?? packageJson.version;
if (input.launcherProtocol !== SERVICE_LAUNCHER_PROTOCOL) {
return {
Expand All@@ -28,6 +33,16 @@ export function runServicePreflight(input: {
"This release requires a newer T3 Code service launcher. Update it on the server machine.",
};
}
try {
await loadNodePty();
} catch (cause) {
const detail = cause instanceof Error ? cause.message : String(cause);
return {
status: "blocked",
version,
reason: `node-pty's native binary is missing from this runtime (${detail}). On Linux it compiles during install; check that npm was allowed to run install scripts and that build tools are present.`,
};
}

return { status: "ready", version, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL };
}
Expand Down
Loading