Skip to content
Merged
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
35 changes: 34 additions & 1 deletion apps/server/src/device/SimlockDeviceHost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SecretProviderId, type SimlockDeviceHostConfig } from "@t3tools/contrac
import * as SecretResolver from "@t3tools/secret-provider-core/server/SecretResolver";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Effect from "effect/Effect";
import * as Clock from "effect/Clock";
import * as Fiber from "effect/Fiber";
import * as Deferred from "effect/Deferred";
import * as Exit from "effect/Exit";
Expand Down Expand Up @@ -56,6 +57,8 @@ const fakePool = (
readonly entered: Deferred.Deferred<void>;
readonly proceed: Deferred.Deferred<void>;
};
/** The gateway's `lease.maxTtlMs`: leases expire one TTL after their last renewal. */
readonly maxTtlMs?: number;
} = {},
) =>
Effect.gen(function* () {
Expand All @@ -66,15 +69,28 @@ const fakePool = (
let leases = 0;
let renewStatus = 200;
let catalog: unknown = CATALOG;
let leaseDeadline = Infinity;
const http = HttpClient.make((request) =>
Effect.sync(() => {
Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis;
const url = new URL(request.url);
requests.push(`${request.method} ${url.pathname}`);
const json = (body: unknown, status = 200) =>
HttpClientResponse.fromWeb(request, Response.json(body, { status }));
if (request.headers.authorization !== "Bearer agent-token")
return json({ error: { code: "UNAUTHENTICATED", message: "Unknown bearer token" } }, 401);
if (url.pathname === "/v1/catalog") return json(catalog);
if (url.pathname.endsWith("/renew") && options.maxTtlMs !== undefined) {
if (now >= leaseDeadline) return json({}, 404);
const body =
request.body._tag === "Uint8Array"
? (JSON.parse(new TextDecoder().decode(request.body.body)) as { ttlMs?: number })
: {};
if ((body.ttlMs ?? 0) > options.maxTtlMs)
return json({ error: { code: "BAD_REQUEST", message: "ttlMs exceeds maxTtlMs" } }, 400);
leaseDeadline = now + (body.ttlMs ?? options.maxTtlMs);
return json({});
}
if (url.pathname.endsWith("/renew")) return json({}, renewStatus);
if (request.method === "DELETE") return json({}, 202);
return json({}, 404);
Expand Down Expand Up @@ -114,6 +130,8 @@ const fakePool = (
path.join(dir, `${session}.json`),
JSON.stringify({ leaseId: `lease-${++leases}` }),
);
if (options.maxTtlMs !== undefined)
leaseDeadline = (yield* Clock.currentTimeMillis) + options.maxTtlMs;
}
if (command === "screenshot") yield* fs.writeFile(args[1]!, new Uint8Array([1, 2, 3]));
return {
Expand Down Expand Up @@ -475,6 +493,21 @@ describe("Simlock device host", () => {
}).pipe(Effect.scoped, Effect.provide(layer)),
);

it.effect("keeps a watched lease renewed when the gateway caps lease TTLs", () =>
Effect.gen(function* () {
const pool = yield* fakePool({ maxTtlMs: 5 * 60_000 });
const host = yield* makeHost(pool, config(), yield* Scope.make());
const ready = yield* host.ensureReady(() => Effect.void);
const [iphone] = (yield* ready.devices.list).devices;
const leaseId = yield* ready.devices.boot(iphone!);

yield* TestClock.adjust("6 minutes");
expect((yield* ready.devices.list).devices.some((device) => device.id === leaseId)).toBe(
true,
);
}).pipe(Effect.scoped, Effect.provide(layer)),
);

it.effect("returns a lease whose open finishes after the host stopped", () =>
Effect.gen(function* () {
const holdOpen = {
Expand Down
5 changes: 2 additions & 3 deletions apps/server/src/device/SimlockDeviceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ const REQUEST_TIMEOUT = Duration.seconds(15);
// The gateway queues a lease request for up to five minutes when every device is busy.
const OPEN_TIMEOUT = Duration.minutes(7);
const COMMAND_TIMEOUT = Duration.minutes(2);
// Renewal keeps a watched but idle device; a lease this server stops renewing ends after LEASE_TTL.
// Renewal keeps a watched but idle device. It sends no ttlMs: Simlock then keeps the lease's own
// length, while one above the gateway's lease.maxTtlMs is refused rather than shortened.
const RENEW_INTERVAL = Duration.minutes(2);
const LEASE_TTL_MS = 10 * 60_000;
const STREAM_PATH = /^\/vendor\/serve-sim\/helper\/([^/]+)\/stream\.avcc$/;
const INPUT_PATH = "/vendor/serve-sim/helper/ws";

Expand Down Expand Up @@ -422,7 +422,6 @@ export const make = Effect.fn("SimlockDeviceHost.make")(function* (
lease.token,
"POST",
`/v1/leases/${encodeURIComponent(lease.leaseId)}/renew`,
{ ttlMs: LEASE_TTL_MS },
).pipe(Effect.result);
if (renewed._tag === "Success") continue;
const error = renewed.failure;
Expand Down
Loading