1tube 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backends/workerd/backend.ts +71 -75
- package/src/server.ts +69 -2
package/package.json
CHANGED
|
@@ -1352,89 +1352,85 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
|
|
|
1352
1352
|
|
|
1353
1353
|
// Preflight: refuse to spawn workerd against ports that are
|
|
1354
1354
|
// already in use. Without this, a leftover workerd from a
|
|
1355
|
-
// previous run
|
|
1356
|
-
// surfaces only as
|
|
1357
|
-
//
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
`--kill-stale-workerd is set, attempting cleanup...`,
|
|
1368
|
-
);
|
|
1355
|
+
// previous run can satisfy the readiness probe and silently
|
|
1356
|
+
// steal our traffic; the failure then surfaces only as stale
|
|
1357
|
+
// response bodies. On reload the still-serving current process
|
|
1358
|
+
// occupies the OTHER slot's ports by design, so probing the next
|
|
1359
|
+
// slot is safe and catches orphaned older generations.
|
|
1360
|
+
let conflicts = probeSocketsFree(capnp.routes);
|
|
1361
|
+
if (conflicts.length > 0 && opts.killStaleWorkerd) {
|
|
1362
|
+
console.log(
|
|
1363
|
+
`[1tube] port preflight found ${conflicts.length} conflict(s)` +
|
|
1364
|
+
(args.gen > 0 ? ` for reload gen=${args.gen}` : "") +
|
|
1365
|
+
`; --kill-stale-workerd is set, attempting cleanup...`,
|
|
1366
|
+
);
|
|
1369
1367
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
1391
|
-
conflicts = probeSocketsFree(capnp.routes);
|
|
1368
|
+
// First try the precise fix: resolve the conflicted port(s)
|
|
1369
|
+
// to their owning PID(s), and kill only owners named workerd.
|
|
1370
|
+
// That is the behavior we want during firmware flashing:
|
|
1371
|
+
// remove the stale candidate that owns 8800, but never kill
|
|
1372
|
+
// a random non-workerd process that happens to be there.
|
|
1373
|
+
const targeted = await killWorkerdOwnersOfPorts(conflicts);
|
|
1374
|
+
if (targeted.attempted) {
|
|
1375
|
+
if (targeted.code === 0) {
|
|
1376
|
+
const details = targeted.stdout?.trim();
|
|
1377
|
+
console.log(
|
|
1378
|
+
`[1tube] targeted stale-workerd cleanup by port complete` +
|
|
1379
|
+
(details ? ` (${details})` : "") +
|
|
1380
|
+
`; re-probing...`,
|
|
1381
|
+
);
|
|
1382
|
+
} else {
|
|
1383
|
+
console.warn(
|
|
1384
|
+
`[1tube] targeted stale-workerd cleanup exited ${targeted.code}` +
|
|
1385
|
+
(targeted.stderr ? `: ${targeted.stderr.trim()}` : ""),
|
|
1386
|
+
);
|
|
1392
1387
|
}
|
|
1388
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1389
|
+
conflicts = probeSocketsFree(capnp.routes);
|
|
1390
|
+
}
|
|
1393
1391
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
`
|
|
1413
|
-
|
|
1414
|
-
);
|
|
1415
|
-
} else {
|
|
1416
|
-
console.warn(
|
|
1417
|
-
`[1tube] ${result.command?.join(" ")} exited ${result.code}` +
|
|
1418
|
-
(result.stderr ? `: ${result.stderr.trim()}` : ""),
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
// Brief settle time: Windows TCP sockets in CLOSE_WAIT can
|
|
1422
|
-
// hold the port for a fraction of a second after the owner
|
|
1423
|
-
// process is killed. 250ms is empirically enough on the
|
|
1424
|
-
// dev boxes we've tested without making the boot path feel
|
|
1425
|
-
// sluggish.
|
|
1426
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
1427
|
-
conflicts = probeSocketsFree(capnp.routes);
|
|
1392
|
+
// If targeted cleanup did not exist on this platform, or it
|
|
1393
|
+
// found no matching workerd owner, fall back to the old typed
|
|
1394
|
+
// process-name cleanup.
|
|
1395
|
+
const result = conflicts.length > 0 ? await killStaleWorkerd() : null;
|
|
1396
|
+
if (result === null) {
|
|
1397
|
+
// Already clear.
|
|
1398
|
+
} else if (result.attempted) {
|
|
1399
|
+
// taskkill returns 128 when no matching image is running —
|
|
1400
|
+
// that's not an error in our context (means the offender
|
|
1401
|
+
// wasn't workerd, which is fine; preflight will catch it).
|
|
1402
|
+
// pkill returns 1 in the same case.
|
|
1403
|
+
const noMatch = (Deno.build.os === "windows" && result.code === 128) ||
|
|
1404
|
+
(Deno.build.os !== "windows" && result.code === 1);
|
|
1405
|
+
if (result.code === 0) {
|
|
1406
|
+
console.log(`[1tube] killed leftover workerd process(es); re-probing...`);
|
|
1407
|
+
} else if (noMatch) {
|
|
1408
|
+
console.log(
|
|
1409
|
+
`[1tube] no leftover workerd processes found; the conflict is held ` +
|
|
1410
|
+
`by something else (re-probing to confirm)...`,
|
|
1411
|
+
);
|
|
1428
1412
|
} else {
|
|
1429
1413
|
console.warn(
|
|
1430
|
-
`[1tube]
|
|
1431
|
-
|
|
1414
|
+
`[1tube] ${result.command?.join(" ")} exited ${result.code}` +
|
|
1415
|
+
(result.stderr ? `: ${result.stderr.trim()}` : ""),
|
|
1432
1416
|
);
|
|
1433
1417
|
}
|
|
1418
|
+
// Brief settle time: Windows TCP sockets in CLOSE_WAIT can
|
|
1419
|
+
// hold the port for a fraction of a second after the owner
|
|
1420
|
+
// process is killed. 250ms is empirically enough on the
|
|
1421
|
+
// dev boxes we've tested without making the boot path feel
|
|
1422
|
+
// sluggish.
|
|
1423
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1424
|
+
conflicts = probeSocketsFree(capnp.routes);
|
|
1425
|
+
} else {
|
|
1426
|
+
console.warn(
|
|
1427
|
+
`[1tube] could not run ${result.command?.join(" ")}: ` +
|
|
1428
|
+
`${result.stderr ?? "unknown error"}`,
|
|
1429
|
+
);
|
|
1434
1430
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1431
|
+
}
|
|
1432
|
+
if (conflicts.length > 0) {
|
|
1433
|
+
throw new Error(formatPortConflictError(conflicts));
|
|
1438
1434
|
}
|
|
1439
1435
|
|
|
1440
1436
|
const inspectorArgs = buildInspectorExtraArgs(opts.inspectorAddr, slot);
|
package/src/server.ts
CHANGED
|
@@ -398,6 +398,15 @@ const LOCAL_SUPABASE_DEFAULTS: Record<string, string> = {
|
|
|
398
398
|
JWT_SECRET: "super-secret-jwt-token-with-at-least-32-characters-long",
|
|
399
399
|
};
|
|
400
400
|
|
|
401
|
+
const LOCAL_SUPABASE_STATUS_KEYS: Record<string, string[]> = {
|
|
402
|
+
SUPABASE_URL: ["API_URL"],
|
|
403
|
+
JWT_SECRET: ["JWT_SECRET"],
|
|
404
|
+
SUPABASE_ANON_KEY: ["ANON_KEY"],
|
|
405
|
+
SUPABASE_PUBLISHABLE_KEY: ["PUBLISHABLE_KEY"],
|
|
406
|
+
SUPABASE_SECRET_KEY: ["SECRET_KEY"],
|
|
407
|
+
SUPABASE_SERVICE_ROLE_KEY: ["SERVICE_ROLE_KEY"],
|
|
408
|
+
};
|
|
409
|
+
|
|
401
410
|
// Only secrets the gateway *itself* uses on the hot path. Anything an edge
|
|
402
411
|
// function happens to need (publishable/secret keys, OPENAI_API_KEY, …) is
|
|
403
412
|
// the function author's contract with the host — refusing to boot because
|
|
@@ -405,8 +414,24 @@ const LOCAL_SUPABASE_DEFAULTS: Record<string, string> = {
|
|
|
405
414
|
// confusing failures.
|
|
406
415
|
const SECRETS_REQUIRED_IN_PROD = ["JWT_SECRET"];
|
|
407
416
|
|
|
408
|
-
function applyDevDefaults() {
|
|
417
|
+
async function applyDevDefaults() {
|
|
409
418
|
let applied = 0;
|
|
419
|
+
const statusEnv = await readLocalSupabaseStatusEnv();
|
|
420
|
+
for (const [target, sources] of Object.entries(LOCAL_SUPABASE_STATUS_KEYS)) {
|
|
421
|
+
if (Deno.env.get(target)) continue;
|
|
422
|
+
const value = sources
|
|
423
|
+
.map((source) => statusEnv[source])
|
|
424
|
+
.find((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
425
|
+
if (!value) continue;
|
|
426
|
+
Deno.env.set(target, value);
|
|
427
|
+
applied++;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!Deno.env.get("SUPABASE_URL") && Deno.env.get("VITE_SUPABASE_URL")) {
|
|
431
|
+
Deno.env.set("SUPABASE_URL", Deno.env.get("VITE_SUPABASE_URL")!);
|
|
432
|
+
applied++;
|
|
433
|
+
}
|
|
434
|
+
|
|
410
435
|
for (const [key, value] of Object.entries(LOCAL_SUPABASE_DEFAULTS)) {
|
|
411
436
|
if (!Deno.env.get(key)) {
|
|
412
437
|
Deno.env.set(key, value);
|
|
@@ -418,6 +443,48 @@ function applyDevDefaults() {
|
|
|
418
443
|
}
|
|
419
444
|
}
|
|
420
445
|
|
|
446
|
+
async function readLocalSupabaseStatusEnv(): Promise<Record<string, string>> {
|
|
447
|
+
const runners: Array<{ command: string; args: string[] }> = [
|
|
448
|
+
{ command: "supabase", args: ["status", "-o", "env"] },
|
|
449
|
+
{ command: "bunx", args: ["supabase", "status", "-o", "env"] },
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
for (const runner of runners) {
|
|
453
|
+
try {
|
|
454
|
+
const output = await new Deno.Command(runner.command, {
|
|
455
|
+
args: runner.args,
|
|
456
|
+
stdout: "piped",
|
|
457
|
+
stderr: "null",
|
|
458
|
+
}).output();
|
|
459
|
+
if (!output.success) continue;
|
|
460
|
+
return parseSupabaseStatusEnv(new TextDecoder().decode(output.stdout));
|
|
461
|
+
} catch {
|
|
462
|
+
// CLI not installed on PATH (or bunx unavailable). Try the next runner.
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function parseSupabaseStatusEnv(text: string): Record<string, string> {
|
|
469
|
+
const out: Record<string, string> = {};
|
|
470
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
471
|
+
const line = rawLine.trim();
|
|
472
|
+
if (!line || line.startsWith("#")) continue;
|
|
473
|
+
const eq = line.indexOf("=");
|
|
474
|
+
if (eq <= 0) continue;
|
|
475
|
+
const key = line.slice(0, eq).trim();
|
|
476
|
+
let value = line.slice(eq + 1).trim();
|
|
477
|
+
if (
|
|
478
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
479
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
480
|
+
) {
|
|
481
|
+
value = value.slice(1, -1);
|
|
482
|
+
}
|
|
483
|
+
out[key] = value;
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
421
488
|
function enforceProdSecrets() {
|
|
422
489
|
// We deliberately do NOT reject "well-known dev default" values
|
|
423
490
|
// here. The local Supabase Docker stack ships with a fixed
|
|
@@ -463,7 +530,7 @@ function requireInternal(c: Context): Response | null {
|
|
|
463
530
|
}
|
|
464
531
|
|
|
465
532
|
if (opts.dev) {
|
|
466
|
-
applyDevDefaults();
|
|
533
|
+
await applyDevDefaults();
|
|
467
534
|
} else {
|
|
468
535
|
enforceProdSecrets();
|
|
469
536
|
}
|