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
57 changes: 57 additions & 0 deletions apps/server/src/provider/providerMaintenanceRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,63 @@ describe("providerMaintenanceRunner", () => {
),
);
});

// Node throws `spawn UNKNOWN` synchronously when Smart App Control blocks the
// executable, and Effect's spawner surfaces that throw as a defect.
it.effect("explains a Windows-blocked updater launch on win32", () =>
Effect.gen(function* () {
const { registry } = yield* makeRegistry(baseOpenCodeProvider);
const runner = yield* makeTestRunner({
...registry,
getProviderMaintenanceCapabilitiesForInstance: () =>
Effect.succeed(
makeProviderMaintenanceCapabilities({
provider: OPENCODE_DRIVER,
packageName: "opencode-ai",
updateExecutable: "vp",
updateArgs: ["i", "-g", "opencode-ai"],
updateLockKey: "vite-plus-global",
}),
),
});

const result = yield* runner.updateProvider(OPENCODE_DRIVER);
const updateState = result.providers[0]?.updateState;

assert.strictEqual(updateState?.status, "failed");
const message = updateState?.message ?? "";
assert.notStrictEqual(message, "spawn UNKNOWN");
assert.include(message, "vp");
assert.include(message, "Windows could not start it");
assert.include(message, "Smart App Control");
}).pipe(
Effect.provide(
Layer.mergeAll(
Layer.succeed(HostProcess.Platform, "win32"),
Layer.succeed(HostProcess.Environment, {
PATH: "C:\\Users\\me\\.vite-plus\\bin",
PATHEXT: ".COM;.EXE;.BAT;.CMD",
}),
Layer.succeed(SpawnExecutableResolution, (command) =>
command === "vp" ? "C:\\Users\\me\\.vite-plus\\bin\\vp.exe" : undefined,
),
layerLatestVersionHttpClient("2.2.6"),
Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() =>
Effect.die(
Object.assign(new Error("spawn UNKNOWN"), {
code: "UNKNOWN",
errno: -4094,
syscall: "spawn",
}),
),
),
),
),
),
),
);
});

it.effect.each([
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/provider/providerMaintenanceRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type ServerProviderUpdatedPayload,
type ServerProviderUpdateState,
} from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { resolveSpawnCommand } from "@t3tools/shared/shell";
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
Expand All @@ -16,6 +17,7 @@ import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as Ref from "effect/Ref";
import * as Schedule from "effect/Schedule";
import * as Schema from "effect/Schema";
Expand Down Expand Up @@ -120,6 +122,27 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR
cause,
}),
),
// Node throws some spawn errors synchronously, such as `spawn UNKNOWN`
// when policy blocks the executable; `mapError` cannot catch those.
Effect.catchDefect((cause) =>
Effect.gen(function* () {
const platform = yield* HostProcess.Platform;
const code =
Predicate.hasProperty(cause, "code") && typeof cause.code === "string"
? cause.code
: null;
const reason =
platform === "win32" && code === "UNKNOWN"
? "Windows could not start it (spawn UNKNOWN). Smart App Control or an application control policy may be blocking this executable."
: code
? `spawn ${code}`
: "the process could not be started";
return yield* new ProviderMaintenanceCommandError({
message: `Failed to run update command ${input.command}: ${reason}`,
cause,
});
}),
),
);
yield* Effect.addFinalizer(() => child.kill().pipe(Effect.ignore));

Expand Down
Loading