diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index 3fc86eea9468b..370d5c8675070 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -17,6 +17,7 @@ import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; @@ -922,6 +923,55 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("Antigravity inst }), ); + it.effect("publishes a Windows release once a transient EPERM on the release rename clears", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const archive = Buffer.from(zipFixtures.windows, "base64"); + const releaseRenameDenied = yield* Deferred.make(); + let releaseRenameDenials = 3; + const { installation } = yield* makeHarness({ + platform: "win32", + archive, + fileSystem: FileSystem.FileSystem.of({ + ...fs, + rename: (source, target) => + Effect.suspend(() => { + if ( + path.basename(path.dirname(target)) !== "versions" || + releaseRenameDenials === 0 + ) { + return fs.rename(source, target); + } + releaseRenameDenials -= 1; + return Deferred.succeed(releaseRenameDenied, undefined).pipe( + Effect.andThen( + Effect.fail( + PlatformError.systemError({ + _tag: "Unknown", + module: "FileSystem", + method: "rename", + pathOrDescriptor: source, + description: "EPERM: operation not permitted, rename", + }), + ), + ), + ); + }), + }), + }); + yield* installation.start; + const terminal = yield* terminalState(installation).pipe(Effect.forkChild); + yield* Deferred.await(releaseRenameDenied); + yield* TestClock.adjust("30 seconds"); + expect(yield* Fiber.join(terminal)).toMatchObject({ + phase: "succeeded", + installedVersion: "fixture-new", + }); + expect((yield* installation.resolve()).version).toBe("fixture-new"); + }), + ); + it.effect("reports unsupported hosts without downloading or changing state", () => Effect.gen(function* () { const { installation, requests } = yield* makeHarness({ asset: null, platform: "darwin" }); diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index 686d1d2947381..c7f09f2c048ed 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -15,6 +15,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; +import type { PlatformError } from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as Schedule from "effect/Schedule"; import * as Scope from "effect/Scope"; @@ -181,6 +182,18 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma ); const versionsDirectory = path.join(managedDirectory, "versions"); const activePath = path.join(managedDirectory, "active.json"); + // Windows briefly keeps handles on fresh runtime files after taskkill or an antivirus scan; retry for up to 10 seconds. + const retryWhileWindowsHoldsHandles = (effect: Effect.Effect) => + effect.pipe( + Effect.retry({ + times: platform === "win32" ? 40 : 0, + schedule: Schedule.spaced("250 millis"), + while: (error) => + error.reason._tag === "PermissionDenied" || + error.reason._tag === "Busy" || + error.reason._tag === "Unknown", + }), + ); const gate = yield* Semaphore.make(1); const leases = new Map(); let running: { readonly operationId: string; readonly fiber: Fiber.Fiber } | undefined; @@ -391,18 +404,9 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma const profileDirectory = yield* Effect.acquireRelease( fs.makeTempDirectory({ prefix: "t3-antigravity-validate-" }), (directory) => - fs.remove(directory, { recursive: true, force: true }).pipe( - // Windows may retain DLL handles briefly after taskkill returns. - Effect.retry({ - times: platform === "win32" ? 10 : 0, - schedule: Schedule.spaced("100 millis"), - while: (error) => - error.reason._tag === "PermissionDenied" || - error.reason._tag === "Busy" || - error.reason._tag === "Unknown", - }), - Effect.orDie, - ), + fs + .remove(directory, { recursive: true, force: true }) + .pipe(retryWhileWindowsHoldsHandles, Effect.orDie), ); const profile = yield* prepareAntigravityProfile({ profileDirectory, @@ -732,6 +736,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma { flag: "wx", mode: 0o600 }, ); yield* fs.rename(pairDirectory, destination).pipe( + retryWhileWindowsHoldsHandles, Effect.catch((cause) => completedRelease(asset.sha256).pipe( Effect.flatMap((existing) =>