diff --git a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts index 243556b6e3b0..a9bea9908329 100644 --- a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts +++ b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts @@ -7,6 +7,7 @@ import { import { afterEach, assert, describe, expect, it, vi } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; import { ServerConfig } from "../config.ts"; import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts"; @@ -115,6 +116,37 @@ describe("ResourceMonitorBinary", () => { }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); + it.effect("repairs a bundled binary that lost its executable bit in npm packaging", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const bundledRoot = path.resolve(import.meta.dirname, "resource-monitor"); + const bundledDir = path.resolve(bundledRoot, "linux-x64"); + const binaryPath = path.resolve(bundledDir, "t3-resource-monitor"); + yield* Effect.acquireRelease(fileSystem.makeDirectory(bundledDir, { recursive: true }), () => + fileSystem.remove(bundledRoot, { recursive: true }).pipe(Effect.ignore), + ); + yield* fileSystem.writeFileString(binaryPath, "binary"); + yield* fileSystem.chmod(binaryPath, 0o644); + + const baseDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-resource-monitor-binary-", + }); + + const service = yield* ResourceMonitorBinary.make().pipe( + Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)), + Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(ResourceMonitorBinary.ResourceMonitorHostLinuxLibc, "gnu"), + Effect.provideService(HostProcessEnvironment, {}), + ); + + assert.equal(yield* service.resolve, binaryPath); + const stat = yield* fileSystem.stat(binaryPath); + assert.notEqual(stat.mode & 0o111, 0); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + it.effect("rejects unsupported platform and architecture pairs", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts index c93bc54a1fba..05eb0260ade6 100644 --- a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts +++ b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts @@ -193,30 +193,51 @@ export const make = Effect.fn("resourceTelemetry.resourceMonitorBinary.make")(fu }); } - const candidates = [...overrideCandidates, ...bundledCandidates]; + const candidates = [ + ...overrideCandidates.map((path) => ({ path, owned: false })), + ...bundledCandidates.map((path) => ({ path, owned: true })), + ]; const resolve: ResourceMonitorBinary["Service"]["resolve"] = Effect.gen(function* () { for (const candidate of candidates) { - const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false)); + const exists = yield* fileSystem + .exists(candidate.path) + .pipe(Effect.orElseSucceed(() => false)); if (!exists) continue; if (platform !== "win32") { - const stat = yield* fileSystem.stat(candidate).pipe(Effect.option); + const stat = yield* fileSystem.stat(candidate.path).pipe(Effect.option); if (Option.isSome(stat) && (stat.value.mode & 0o111) === 0) { + // npm only preserves the executable bit for files listed in + // package.json's `bin` field, so our bundled sidecars land on disk + // as 0644 no matter what CI chmods before publish. We own these + // paths, so repair the bit instead of leaving telemetry disabled + // forever. A user-supplied override path is left alone: staying + // fail-closed there prevents chmod'ing an arbitrary file we didn't + // build. + if (candidate.owned) { + const repaired = yield* fileSystem.chmod(candidate.path, stat.value.mode | 0o111).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ); + if (repaired) { + return candidate.path; + } + } return yield* new ResourceMonitorBinaryNotExecutable({ - path: candidate, + path: candidate.path, mode: stat.value.mode, }); } } - return candidate; + return candidate.path; } return yield* new ResourceMonitorBinaryNotFound({ platform, architecture, - candidates, + candidates: candidates.map((candidate) => candidate.path), }); });