| ... | @@ -4,6 +4,7 @@ import { describe, expect, it } from "@effect/vitest"; | ... | @@ -4,6 +4,7 @@ import { describe, expect, it } from "@effect/vitest"; |
| 4 | import * as ConfigProvider from "effect/ConfigProvider"; | 4 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 5 | import * as Deferred from "effect/Deferred"; | 5 | import * as Deferred from "effect/Deferred"; |
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| | 7 | import * as Fiber from "effect/Fiber"; |
| 7 | import * as Hex from "effect/encoding/Hex"; | 8 | import * as Hex from "effect/encoding/Hex"; |
| 8 | import * as Fiber from "effect/Fiber"; | 9 | import * as Fiber from "effect/Fiber"; |
| 9 | import * as FileSystem from "effect/FileSystem"; | 10 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -154,6 +155,133 @@ const managedDirectory = (baseDir: string, platform: NodeJS.Platform) => | ... | @@ -154,6 +155,133 @@ const managedDirectory = (baseDir: string, platform: NodeJS.Platform) => |
| 154 | `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/${platform}-x64`; | 155 | `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/${platform}-x64`; |
| 155 | | 156 | |
| 156 | describe("RelayClient", () => { | 157 | describe("RelayClient", () => { |
| | 158 | it.effect("cancels a contended install without removing the other installer's lock", () => |
| | 159 | Effect.gen(function* () { |
| | 160 | const fileSystem = yield* FileSystem.FileSystem; |
| | 161 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" }); |
| | 162 | const directory = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64`; |
| | 163 | const lockPath = `${directory}/cloudflared.lock`; |
| | 164 | yield* fileSystem.makeDirectory(directory, { recursive: true }); |
| | 165 | yield* fileSystem.writeFileString(lockPath, "other-installer"); |
| | 166 | const contended = yield* Deferred.make<void>(); |
| | 167 | const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir }).pipe( |
| | 168 | Effect.provideService(FileSystem.FileSystem, { |
| | 169 | ...fileSystem, |
| | 170 | writeFileString: (path, contents, options) => |
| | 171 | fileSystem |
| | 172 | .writeFileString(path, contents, options) |
| | 173 | .pipe( |
| | 174 | Effect.tapError(() => |
| | 175 | path === lockPath ? Deferred.succeed(contended, undefined) : Effect.void, |
| | 176 | ), |
| | 177 | ), |
| | 178 | }), |
| | 179 | ); |
| | 180 | const installing = yield* manager.install.pipe(Effect.forkChild); |
| | 181 | yield* Deferred.await(contended); |
| | 182 | yield* Fiber.interrupt(installing); |
| | 183 | expect(yield* fileSystem.readFileString(lockPath)).toBe("other-installer"); |
| | 184 | }).pipe( |
| | 185 | Effect.scoped, |
| | 186 | Effect.provide( |
| | 187 | Layer.mergeAll( |
| | 188 | NodeServices.layer, |
| | 189 | layerHttpClient(new Uint8Array()), |
| | 190 | layerSpawner([]), |
| | 191 | layerHostRuntime({ PATH: "" }), |
| | 192 | ), |
| | 193 | ), |
| | 194 | ), |
| | 195 | ); |
| | 196 | |
| | 197 | it.effect("releases the install lock when a download is cancelled", () => |
| | 198 | Effect.gen(function* () { |
| | 199 | const fileSystem = yield* FileSystem.FileSystem; |
| | 200 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" }); |
| | 201 | const lockPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`; |
| | 202 | const downloading = yield* Deferred.make<void>(); |
| | 203 | const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir }); |
| | 204 | const installing = yield* manager |
| | 205 | .installWithProgress((event) => |
| | 206 | event.type === "progress" && event.stage === "downloading" |
| | 207 | ? Deferred.succeed(downloading, undefined).pipe(Effect.andThen(Effect.never)) |
| | 208 | : Effect.void, |
| | 209 | ) |
| | 210 | .pipe(Effect.forkChild); |
| | 211 | yield* Deferred.await(downloading); |
| | 212 | expect(yield* fileSystem.exists(lockPath)).toBe(true); |
| | 213 | yield* Fiber.interrupt(installing); |
| | 214 | expect(yield* fileSystem.exists(lockPath)).toBe(false); |
| | 215 | }).pipe( |
| | 216 | Effect.scoped, |
| | 217 | Effect.provide( |
| | 218 | Layer.mergeAll( |
| | 219 | NodeServices.layer, |
| | 220 | layerHttpClient(new Uint8Array()), |
| | 221 | layerSpawner([]), |
| | 222 | layerHostRuntime({ PATH: "" }), |
| | 223 | ), |
| | 224 | ), |
| | 225 | ), |
| | 226 | ); |
| | 227 | |
| | 228 | it.effect.skipIf(windowsHost)("releases a lock acquired while installation is cancelled", () => |
| | 229 | Effect.gen(function* () { |
| | 230 | const fileSystem = yield* FileSystem.FileSystem; |
| | 231 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" }); |
| | 232 | const lockPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`; |
| | 233 | const acquired = yield* Deferred.make<void>(); |
| | 234 | const completeWrite = yield* Deferred.make<void>(); |
| | 235 | let pauseWrite = true; |
| | 236 | const manager = yield* RelayClient.makeCloudflaredRelayClient({ |
| | 237 | baseDir, |
| | 238 | releaseAsset: { |
| | 239 | url: "https://example.test/cloudflared", |
| | 240 | sha256: Hex.encode(sha256(new TextEncoder().encode("test-binary"))), |
| | 241 | archive: "binary", |
| | 242 | }, |
| | 243 | }).pipe( |
| | 244 | Effect.provideService(FileSystem.FileSystem, { |
| | 245 | ...fileSystem, |
| | 246 | writeFileString: (path, contents, options) => |
| | 247 | fileSystem |
| | 248 | .writeFileString(path, contents, options) |
| | 249 | .pipe( |
| | 250 | Effect.tap(() => |
| | 251 | path === lockPath && pauseWrite |
| | 252 | ? Deferred.succeed(acquired, undefined).pipe( |
| | 253 | Effect.andThen(Deferred.await(completeWrite)), |
| | 254 | ) |
| | 255 | : Effect.void, |
| | 256 | ), |
| | 257 | ), |
| | 258 | }), |
| | 259 | ); |
| | 260 | |
| | 261 | const installing = yield* manager.install.pipe(Effect.forkChild); |
| | 262 | yield* Deferred.await(acquired); |
| | 263 | const cancelling = yield* Fiber.interrupt(installing).pipe( |
| | 264 | Effect.forkChild({ startImmediately: true }), |
| | 265 | ); |
| | 266 | yield* Deferred.succeed(completeWrite, undefined); |
| | 267 | yield* Fiber.join(cancelling); |
| | 268 | expect(yield* fileSystem.exists(lockPath)).toBe(false); |
| | 269 | |
| | 270 | pauseWrite = false; |
| | 271 | expect(yield* manager.install).toMatchObject({ status: "available" }); |
| | 272 | expect(yield* fileSystem.exists(lockPath)).toBe(false); |
| | 273 | }).pipe( |
| | 274 | Effect.scoped, |
| | 275 | Effect.provide( |
| | 276 | Layer.mergeAll( |
| | 277 | NodeServices.layer, |
| | 278 | layerHttpClient(new TextEncoder().encode("test-binary")), |
| | 279 | layerSpawner([]), |
| | 280 | layerHostRuntime({ PATH: "" }), |
| | 281 | ), |
| | 282 | ), |
| | 283 | ), |
| | 284 | ); |
| 157 | it.effect.skipIf(windowsHost)( | 285 | it.effect.skipIf(windowsHost)( |
| 158 | "resolves explicit overrides before managed and PATH executables", | 286 | "resolves explicit overrides before managed and PATH executables", |
| 159 | () => | 287 | () => |
| ... | @@ -274,6 +402,11 @@ describe("RelayClient", () => { | ... | @@ -274,6 +402,11 @@ describe("RelayClient", () => { |
| 274 | const error = yield* manager.install.pipe(Effect.flip); | 402 | const error = yield* manager.install.pipe(Effect.flip); |
| 275 | expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError); | 403 | expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError); |
| 276 | expect(error.reason).toBe("invalid_checksum"); | 404 | expect(error.reason).toBe("invalid_checksum"); |
| | 405 | expect( |
| | 406 | yield* fileSystem.exists( |
| | 407 | `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`, |
| | 408 | ), |
| | 409 | ).toBe(false); |
| 277 | }).pipe( | 410 | }).pipe( |
| 278 | Effect.scoped, | 411 | Effect.scoped, |
| 279 | Effect.provide( | 412 | Effect.provide( |