rechrome 1.32.0 → 1.33.0
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/rechrome.js +104 -3
- package/rechrome.ts +104 -3
- package/serve.js +16 -0
- package/serve.ts +16 -0
package/package.json
CHANGED
package/rechrome.js
CHANGED
|
@@ -377,6 +377,9 @@ export function buildListenChoices(interfaces: ReturnType<typeof networkInterfac
|
|
|
377
377
|
return choices.sort((a, b) => rank[a.kind] - rank[b.kind]);
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// The Mac app's binary only acts as the CLI when it is told to (or has a terminal): from a
|
|
381
|
+
// daemon it tries to start the GUI and fails. Harmless for the standalone CLI.
|
|
382
|
+
const TAILSCALE_ENV = { ...process.env, TAILSCALE_BE_CLI: "1" };
|
|
380
383
|
const tailscaleBinary = () => Bun.which("tailscale") || (existsSync("/Applications/Tailscale.app/Contents/MacOS/Tailscale") ? "/Applications/Tailscale.app/Contents/MacOS/Tailscale" : null);
|
|
381
384
|
|
|
382
385
|
/** Run the Tailscale CLI read-only; null when it is missing, disconnected, or slow. */
|
|
@@ -384,7 +387,7 @@ async function runTailscale(args: string[]): Promise<string | null> {
|
|
|
384
387
|
const binary = tailscaleBinary();
|
|
385
388
|
if (!binary) return null;
|
|
386
389
|
try {
|
|
387
|
-
const proc = Bun.spawn([binary, ...args], { stdout: "pipe", stderr: "ignore" });
|
|
390
|
+
const proc = Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "pipe", stderr: "ignore" });
|
|
388
391
|
const timeout = setTimeout(() => proc.kill(), 2000);
|
|
389
392
|
try {
|
|
390
393
|
const output = await new Response(proc.stdout).text();
|
|
@@ -445,6 +448,101 @@ export async function detectTailscaleServe(port: number, prefix: string): Promis
|
|
|
445
448
|
return { dnsName, routeUrl, ...serveMountsAndFunnel(serveStatus, routeUrl ? new URL(routeUrl).hostname : dnsName) };
|
|
446
449
|
}
|
|
447
450
|
|
|
451
|
+
type ServeHandler = { hostPort: string; mount: string; target: URL };
|
|
452
|
+
|
|
453
|
+
/** Every Serve route that proxies to a loopback HTTP port. */
|
|
454
|
+
function loopbackServeHandlers(serveStatus: unknown): ServeHandler[] {
|
|
455
|
+
const web = (serveStatus as { Web?: Record<string, { Handlers?: Record<string, { Proxy?: string }> }> } | null)?.Web ?? {};
|
|
456
|
+
const handlers: ServeHandler[] = [];
|
|
457
|
+
for (const [hostPort, config] of Object.entries(web)) {
|
|
458
|
+
for (const [path, handler] of Object.entries(config.Handlers ?? {})) {
|
|
459
|
+
if (!handler.Proxy) continue;
|
|
460
|
+
let target: URL;
|
|
461
|
+
try { target = new URL(/^[a-z]+:\/\//i.test(handler.Proxy) ? handler.Proxy : `http://${handler.Proxy}`); } catch { continue; }
|
|
462
|
+
if (!["127.0.0.1", "localhost", "[::1]"].includes(target.hostname) || !target.port) continue;
|
|
463
|
+
handlers.push({ hostPort, mount: path.replace(/\/+$/, "") || "/", target });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return handlers;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export type ServeRepair = { hostPort: string; mount: string; from: number; to: number; proxy: string };
|
|
470
|
+
export type ServeStale = { hostPort: string; mount: string; port: number };
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Health check for Tailscale Serve against listeners.json. `repair`: a route on a listener's own
|
|
474
|
+
* mount that proxies to another port (the listener moved), re-pointed at the listener's port.
|
|
475
|
+
* `stale`: a route next to a listener's mount (same first path segment, e.g. /rechrome beside
|
|
476
|
+
* /rechrome/taku) whose port no listener uses — a removed listener's leftover, which answers 502.
|
|
477
|
+
* Root ("/") mounts are never touched: they could be anything.
|
|
478
|
+
*/
|
|
479
|
+
export function planTailscaleServeRepairs(serveStatus: unknown, listeners: { port: number; prefix?: string }[]): { repair: ServeRepair[]; stale: ServeStale[] } {
|
|
480
|
+
const ports = new Set(listeners.map(l => l.port));
|
|
481
|
+
const mounts = new Map<string, number>();
|
|
482
|
+
for (const l of listeners) {
|
|
483
|
+
const mount = normalizePrefix(l.prefix).replace(/\/+$/, "");
|
|
484
|
+
if (mount) mounts.set(mount, l.port);
|
|
485
|
+
}
|
|
486
|
+
const roots = new Set([...mounts.keys()].map(m => m.split("/")[1]));
|
|
487
|
+
const repair: ServeRepair[] = [], stale: ServeStale[] = [];
|
|
488
|
+
for (const { hostPort, mount, target } of loopbackServeHandlers(serveStatus)) {
|
|
489
|
+
const port = Number(target.port), want = mounts.get(mount);
|
|
490
|
+
if (want !== undefined) {
|
|
491
|
+
if (port !== want) {
|
|
492
|
+
const proxy = new URL(target); proxy.port = String(want);
|
|
493
|
+
repair.push({ hostPort, mount, from: port, to: want, proxy: proxy.href.replace(/\/$/, mount === "/" ? "/" : "") });
|
|
494
|
+
}
|
|
495
|
+
} else if (mount !== "/" && roots.has(mount.split("/")[1]) && !ports.has(port)) {
|
|
496
|
+
stale.push({ hostPort, mount, port });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { repair, stale };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Drop the Serve routes that proxy to a removed listener, so they don't linger answering 502. */
|
|
503
|
+
async function removeTailscaleServeRoutes(listener: { port: number; prefix?: string }): Promise<void> {
|
|
504
|
+
const binary = tailscaleBinary(), raw = binary && await runTailscale(["serve", "status", "--json"]);
|
|
505
|
+
if (!binary || !raw) return;
|
|
506
|
+
let serveStatus: unknown;
|
|
507
|
+
try { serveStatus = JSON.parse(raw); } catch { return; }
|
|
508
|
+
const mount = normalizePrefix(listener.prefix).replace(/\/+$/, "") || "/";
|
|
509
|
+
for (const h of loopbackServeHandlers(serveStatus)) {
|
|
510
|
+
if (Number(h.target.port) !== listener.port || h.mount !== mount) continue;
|
|
511
|
+
const args = ["serve", `--https=${h.hostPort.split(/:(?=\d+$)/)[1] ?? "443"}`, `--set-path=${h.mount}`, "off"];
|
|
512
|
+
const ok = await Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "ignore", stderr: "inherit", windowsHide: true }).exited === 0;
|
|
513
|
+
console.log(ok ? `Removed its Tailscale Serve route ${h.hostPort}${h.mount}.` : `Remove its Tailscale Serve route yourself: tailscale ${args.join(" ")}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Daemon health check: keep Tailscale Serve routes pointed at the listeners' current ports.
|
|
519
|
+
* Re-points a moved listener's route on this node; only reports stale routes (removing a
|
|
520
|
+
* route is the user's call). Returns log lines, empty when healthy or Tailscale is absent.
|
|
521
|
+
*/
|
|
522
|
+
export async function checkTailscaleServe(listeners: { port: number; prefix?: string }[]): Promise<string[]> {
|
|
523
|
+
const binary = tailscaleBinary();
|
|
524
|
+
if (!binary) return [];
|
|
525
|
+
const raw = await runTailscale(["serve", "status", "--json"]);
|
|
526
|
+
if (!raw) return [];
|
|
527
|
+
let serveStatus: unknown;
|
|
528
|
+
try { serveStatus = JSON.parse(raw); } catch { return []; }
|
|
529
|
+
const { repair, stale } = planTailscaleServeRepairs(serveStatus, listeners);
|
|
530
|
+
const lines: string[] = [];
|
|
531
|
+
for (const r of repair) {
|
|
532
|
+
const httpsPort = r.hostPort.split(/:(?=\d+$)/)[1] ?? "443";
|
|
533
|
+
const args = ["serve", "--bg", `--https=${httpsPort}`, `--set-path=${r.mount}`, r.proxy];
|
|
534
|
+
const proc = Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "ignore", stderr: "pipe", windowsHide: true });
|
|
535
|
+
const ok = await proc.exited === 0;
|
|
536
|
+
lines.push(ok
|
|
537
|
+
? `tailscale serve: re-pointed ${r.hostPort}${r.mount} from 127.0.0.1:${r.from} to ${r.to}`
|
|
538
|
+
: `tailscale serve: ${r.hostPort}${r.mount} points at 127.0.0.1:${r.from}, listener is on ${r.to}; re-pointing failed (${(await new Response(proc.stderr).text()).trim()}). Run: tailscale ${args.join(" ")}`);
|
|
539
|
+
}
|
|
540
|
+
for (const s of stale) {
|
|
541
|
+
lines.push(`tailscale serve: ${s.hostPort}${s.mount} proxies to 127.0.0.1:${s.port}, where no listener runs (clients get HTTP 502). Remove it: tailscale serve --https=${s.hostPort.split(/:(?=\d+$)/)[1] ?? "443"} --set-path=${s.mount} off`);
|
|
542
|
+
}
|
|
543
|
+
return lines;
|
|
544
|
+
}
|
|
545
|
+
|
|
448
546
|
/** The same connection (key and profile) at another base URL, e.g. where a proxy exposes the listener. */
|
|
449
547
|
export function rebaseConnectionUrl(routeUrl: string, localUrl: string): string {
|
|
450
548
|
const local = new URL(localUrl);
|
|
@@ -1408,7 +1506,7 @@ async function exposeWithTailscaleServe(listener: Listener): Promise<string | nu
|
|
|
1408
1506
|
const mount = prefix === "/" ? "/" : prefix.slice(0, -1);
|
|
1409
1507
|
const args = ["serve", "--bg", `--set-path=${mount}`, `http://127.0.0.1:${listener.port}${mount === "/" ? "" : mount}`];
|
|
1410
1508
|
console.error(`Running: tailscale ${args.join(" ")}`);
|
|
1411
|
-
const proc = Bun.spawn([binary, ...args], { stdout: "ignore", stderr: "inherit", windowsHide: true });
|
|
1509
|
+
const proc = Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "ignore", stderr: "inherit", windowsHide: true });
|
|
1412
1510
|
if (await proc.exited !== 0) {
|
|
1413
1511
|
console.error(`tailscale serve failed. Run it yourself, then rech share again:\n tailscale ${args.join(" ")}`);
|
|
1414
1512
|
return null;
|
|
@@ -1669,7 +1767,9 @@ async function callServe(
|
|
|
1669
1767
|
return JSON.parse(text);
|
|
1670
1768
|
} catch {
|
|
1671
1769
|
// Not the daemon answering, e.g. a reverse proxy's 404 because the URL's path prefix is wrong.
|
|
1672
|
-
|
|
1770
|
+
let detail = `HTTP ${res.status} from ${serviceUrl(url, "run")} is not a rechrome daemon response: ${text.slice(0, 200).trim()}`;
|
|
1771
|
+
if (res.status === 502 || res.status === 503 || res.status === 504)
|
|
1772
|
+
detail += `\n The proxy is up but nothing answers behind it: this link's listener was likely removed or moved. Ask its owner for a fresh link (\`rech share\`), then \`rechrome connect <link>\`.`;
|
|
1673
1773
|
if (throwOnFailure) throw new Error(detail);
|
|
1674
1774
|
console.error(`[rech] rech-client -> ${serviceUrl(url, "run")}\n -x: ${detail}`);
|
|
1675
1775
|
process.exit(1);
|
|
@@ -2759,6 +2859,7 @@ async function removeListener(name: string): Promise<void> {
|
|
|
2759
2859
|
config.listeners = config.listeners.filter(l => l !== listener);
|
|
2760
2860
|
await writeListeners(config);
|
|
2761
2861
|
console.log("Listener removed from configuration; daemon reloads automatically.");
|
|
2862
|
+
await removeTailscaleServeRoutes(listener);
|
|
2762
2863
|
}
|
|
2763
2864
|
|
|
2764
2865
|
async function resolveProfileKeys(selectors: string[]): Promise<string[]> {
|
package/rechrome.ts
CHANGED
|
@@ -377,6 +377,9 @@ export function buildListenChoices(interfaces: ReturnType<typeof networkInterfac
|
|
|
377
377
|
return choices.sort((a, b) => rank[a.kind] - rank[b.kind]);
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// The Mac app's binary only acts as the CLI when it is told to (or has a terminal): from a
|
|
381
|
+
// daemon it tries to start the GUI and fails. Harmless for the standalone CLI.
|
|
382
|
+
const TAILSCALE_ENV = { ...process.env, TAILSCALE_BE_CLI: "1" };
|
|
380
383
|
const tailscaleBinary = () => Bun.which("tailscale") || (existsSync("/Applications/Tailscale.app/Contents/MacOS/Tailscale") ? "/Applications/Tailscale.app/Contents/MacOS/Tailscale" : null);
|
|
381
384
|
|
|
382
385
|
/** Run the Tailscale CLI read-only; null when it is missing, disconnected, or slow. */
|
|
@@ -384,7 +387,7 @@ async function runTailscale(args: string[]): Promise<string | null> {
|
|
|
384
387
|
const binary = tailscaleBinary();
|
|
385
388
|
if (!binary) return null;
|
|
386
389
|
try {
|
|
387
|
-
const proc = Bun.spawn([binary, ...args], { stdout: "pipe", stderr: "ignore" });
|
|
390
|
+
const proc = Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "pipe", stderr: "ignore" });
|
|
388
391
|
const timeout = setTimeout(() => proc.kill(), 2000);
|
|
389
392
|
try {
|
|
390
393
|
const output = await new Response(proc.stdout).text();
|
|
@@ -445,6 +448,101 @@ export async function detectTailscaleServe(port: number, prefix: string): Promis
|
|
|
445
448
|
return { dnsName, routeUrl, ...serveMountsAndFunnel(serveStatus, routeUrl ? new URL(routeUrl).hostname : dnsName) };
|
|
446
449
|
}
|
|
447
450
|
|
|
451
|
+
type ServeHandler = { hostPort: string; mount: string; target: URL };
|
|
452
|
+
|
|
453
|
+
/** Every Serve route that proxies to a loopback HTTP port. */
|
|
454
|
+
function loopbackServeHandlers(serveStatus: unknown): ServeHandler[] {
|
|
455
|
+
const web = (serveStatus as { Web?: Record<string, { Handlers?: Record<string, { Proxy?: string }> }> } | null)?.Web ?? {};
|
|
456
|
+
const handlers: ServeHandler[] = [];
|
|
457
|
+
for (const [hostPort, config] of Object.entries(web)) {
|
|
458
|
+
for (const [path, handler] of Object.entries(config.Handlers ?? {})) {
|
|
459
|
+
if (!handler.Proxy) continue;
|
|
460
|
+
let target: URL;
|
|
461
|
+
try { target = new URL(/^[a-z]+:\/\//i.test(handler.Proxy) ? handler.Proxy : `http://${handler.Proxy}`); } catch { continue; }
|
|
462
|
+
if (!["127.0.0.1", "localhost", "[::1]"].includes(target.hostname) || !target.port) continue;
|
|
463
|
+
handlers.push({ hostPort, mount: path.replace(/\/+$/, "") || "/", target });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return handlers;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export type ServeRepair = { hostPort: string; mount: string; from: number; to: number; proxy: string };
|
|
470
|
+
export type ServeStale = { hostPort: string; mount: string; port: number };
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Health check for Tailscale Serve against listeners.json. `repair`: a route on a listener's own
|
|
474
|
+
* mount that proxies to another port (the listener moved), re-pointed at the listener's port.
|
|
475
|
+
* `stale`: a route next to a listener's mount (same first path segment, e.g. /rechrome beside
|
|
476
|
+
* /rechrome/taku) whose port no listener uses — a removed listener's leftover, which answers 502.
|
|
477
|
+
* Root ("/") mounts are never touched: they could be anything.
|
|
478
|
+
*/
|
|
479
|
+
export function planTailscaleServeRepairs(serveStatus: unknown, listeners: { port: number; prefix?: string }[]): { repair: ServeRepair[]; stale: ServeStale[] } {
|
|
480
|
+
const ports = new Set(listeners.map(l => l.port));
|
|
481
|
+
const mounts = new Map<string, number>();
|
|
482
|
+
for (const l of listeners) {
|
|
483
|
+
const mount = normalizePrefix(l.prefix).replace(/\/+$/, "");
|
|
484
|
+
if (mount) mounts.set(mount, l.port);
|
|
485
|
+
}
|
|
486
|
+
const roots = new Set([...mounts.keys()].map(m => m.split("/")[1]));
|
|
487
|
+
const repair: ServeRepair[] = [], stale: ServeStale[] = [];
|
|
488
|
+
for (const { hostPort, mount, target } of loopbackServeHandlers(serveStatus)) {
|
|
489
|
+
const port = Number(target.port), want = mounts.get(mount);
|
|
490
|
+
if (want !== undefined) {
|
|
491
|
+
if (port !== want) {
|
|
492
|
+
const proxy = new URL(target); proxy.port = String(want);
|
|
493
|
+
repair.push({ hostPort, mount, from: port, to: want, proxy: proxy.href.replace(/\/$/, mount === "/" ? "/" : "") });
|
|
494
|
+
}
|
|
495
|
+
} else if (mount !== "/" && roots.has(mount.split("/")[1]) && !ports.has(port)) {
|
|
496
|
+
stale.push({ hostPort, mount, port });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { repair, stale };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Drop the Serve routes that proxy to a removed listener, so they don't linger answering 502. */
|
|
503
|
+
async function removeTailscaleServeRoutes(listener: { port: number; prefix?: string }): Promise<void> {
|
|
504
|
+
const binary = tailscaleBinary(), raw = binary && await runTailscale(["serve", "status", "--json"]);
|
|
505
|
+
if (!binary || !raw) return;
|
|
506
|
+
let serveStatus: unknown;
|
|
507
|
+
try { serveStatus = JSON.parse(raw); } catch { return; }
|
|
508
|
+
const mount = normalizePrefix(listener.prefix).replace(/\/+$/, "") || "/";
|
|
509
|
+
for (const h of loopbackServeHandlers(serveStatus)) {
|
|
510
|
+
if (Number(h.target.port) !== listener.port || h.mount !== mount) continue;
|
|
511
|
+
const args = ["serve", `--https=${h.hostPort.split(/:(?=\d+$)/)[1] ?? "443"}`, `--set-path=${h.mount}`, "off"];
|
|
512
|
+
const ok = await Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "ignore", stderr: "inherit", windowsHide: true }).exited === 0;
|
|
513
|
+
console.log(ok ? `Removed its Tailscale Serve route ${h.hostPort}${h.mount}.` : `Remove its Tailscale Serve route yourself: tailscale ${args.join(" ")}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Daemon health check: keep Tailscale Serve routes pointed at the listeners' current ports.
|
|
519
|
+
* Re-points a moved listener's route on this node; only reports stale routes (removing a
|
|
520
|
+
* route is the user's call). Returns log lines, empty when healthy or Tailscale is absent.
|
|
521
|
+
*/
|
|
522
|
+
export async function checkTailscaleServe(listeners: { port: number; prefix?: string }[]): Promise<string[]> {
|
|
523
|
+
const binary = tailscaleBinary();
|
|
524
|
+
if (!binary) return [];
|
|
525
|
+
const raw = await runTailscale(["serve", "status", "--json"]);
|
|
526
|
+
if (!raw) return [];
|
|
527
|
+
let serveStatus: unknown;
|
|
528
|
+
try { serveStatus = JSON.parse(raw); } catch { return []; }
|
|
529
|
+
const { repair, stale } = planTailscaleServeRepairs(serveStatus, listeners);
|
|
530
|
+
const lines: string[] = [];
|
|
531
|
+
for (const r of repair) {
|
|
532
|
+
const httpsPort = r.hostPort.split(/:(?=\d+$)/)[1] ?? "443";
|
|
533
|
+
const args = ["serve", "--bg", `--https=${httpsPort}`, `--set-path=${r.mount}`, r.proxy];
|
|
534
|
+
const proc = Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "ignore", stderr: "pipe", windowsHide: true });
|
|
535
|
+
const ok = await proc.exited === 0;
|
|
536
|
+
lines.push(ok
|
|
537
|
+
? `tailscale serve: re-pointed ${r.hostPort}${r.mount} from 127.0.0.1:${r.from} to ${r.to}`
|
|
538
|
+
: `tailscale serve: ${r.hostPort}${r.mount} points at 127.0.0.1:${r.from}, listener is on ${r.to}; re-pointing failed (${(await new Response(proc.stderr).text()).trim()}). Run: tailscale ${args.join(" ")}`);
|
|
539
|
+
}
|
|
540
|
+
for (const s of stale) {
|
|
541
|
+
lines.push(`tailscale serve: ${s.hostPort}${s.mount} proxies to 127.0.0.1:${s.port}, where no listener runs (clients get HTTP 502). Remove it: tailscale serve --https=${s.hostPort.split(/:(?=\d+$)/)[1] ?? "443"} --set-path=${s.mount} off`);
|
|
542
|
+
}
|
|
543
|
+
return lines;
|
|
544
|
+
}
|
|
545
|
+
|
|
448
546
|
/** The same connection (key and profile) at another base URL, e.g. where a proxy exposes the listener. */
|
|
449
547
|
export function rebaseConnectionUrl(routeUrl: string, localUrl: string): string {
|
|
450
548
|
const local = new URL(localUrl);
|
|
@@ -1408,7 +1506,7 @@ async function exposeWithTailscaleServe(listener: Listener): Promise<string | nu
|
|
|
1408
1506
|
const mount = prefix === "/" ? "/" : prefix.slice(0, -1);
|
|
1409
1507
|
const args = ["serve", "--bg", `--set-path=${mount}`, `http://127.0.0.1:${listener.port}${mount === "/" ? "" : mount}`];
|
|
1410
1508
|
console.error(`Running: tailscale ${args.join(" ")}`);
|
|
1411
|
-
const proc = Bun.spawn([binary, ...args], { stdout: "ignore", stderr: "inherit", windowsHide: true });
|
|
1509
|
+
const proc = Bun.spawn([binary, ...args], { env: TAILSCALE_ENV, stdout: "ignore", stderr: "inherit", windowsHide: true });
|
|
1412
1510
|
if (await proc.exited !== 0) {
|
|
1413
1511
|
console.error(`tailscale serve failed. Run it yourself, then rech share again:\n tailscale ${args.join(" ")}`);
|
|
1414
1512
|
return null;
|
|
@@ -1669,7 +1767,9 @@ async function callServe(
|
|
|
1669
1767
|
return JSON.parse(text);
|
|
1670
1768
|
} catch {
|
|
1671
1769
|
// Not the daemon answering, e.g. a reverse proxy's 404 because the URL's path prefix is wrong.
|
|
1672
|
-
|
|
1770
|
+
let detail = `HTTP ${res.status} from ${serviceUrl(url, "run")} is not a rechrome daemon response: ${text.slice(0, 200).trim()}`;
|
|
1771
|
+
if (res.status === 502 || res.status === 503 || res.status === 504)
|
|
1772
|
+
detail += `\n The proxy is up but nothing answers behind it: this link's listener was likely removed or moved. Ask its owner for a fresh link (\`rech share\`), then \`rechrome connect <link>\`.`;
|
|
1673
1773
|
if (throwOnFailure) throw new Error(detail);
|
|
1674
1774
|
console.error(`[rech] rech-client -> ${serviceUrl(url, "run")}\n -x: ${detail}`);
|
|
1675
1775
|
process.exit(1);
|
|
@@ -2759,6 +2859,7 @@ async function removeListener(name: string): Promise<void> {
|
|
|
2759
2859
|
config.listeners = config.listeners.filter(l => l !== listener);
|
|
2760
2860
|
await writeListeners(config);
|
|
2761
2861
|
console.log("Listener removed from configuration; daemon reloads automatically.");
|
|
2862
|
+
await removeTailscaleServeRoutes(listener);
|
|
2762
2863
|
}
|
|
2763
2864
|
|
|
2764
2865
|
async function resolveProfileKeys(selectors: string[]): Promise<string[]> {
|
package/serve.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolvePlaywrightCli,
|
|
18
18
|
readTokenRegistry,
|
|
19
19
|
readChromeProfileCache,
|
|
20
|
+
checkTailscaleServe,
|
|
20
21
|
} from "./rechrome.js";
|
|
21
22
|
|
|
22
23
|
const TAILSCALE_BIN = process.env.TAILSCALE_BIN || "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
@@ -1041,4 +1042,19 @@ export async function serve() {
|
|
|
1041
1042
|
} finally { reloading = false; }
|
|
1042
1043
|
}, 1000);
|
|
1043
1044
|
log("Connection credentials remain in local configuration; listener changes reload automatically");
|
|
1045
|
+
// Keep Tailscale Serve routes on the listeners' current ports: right away, after every listener
|
|
1046
|
+
// change, and every few minutes (routes can be edited by hand). Each finding is logged once.
|
|
1047
|
+
let checkedFor = "", lastReport = "", lastCheck = 0, checking = false;
|
|
1048
|
+
setInterval(async () => {
|
|
1049
|
+
if (checking || (applied === checkedFor && Date.now() - lastCheck < 5 * 60_000)) return;
|
|
1050
|
+
checking = true;
|
|
1051
|
+
try {
|
|
1052
|
+
const listeners = JSON.parse(applied || "[]") as Listener[];
|
|
1053
|
+
const lines = await checkTailscaleServe(listeners);
|
|
1054
|
+
const report = lines.join("\n");
|
|
1055
|
+
if (report && report !== lastReport) for (const line of lines) log(line);
|
|
1056
|
+
lastReport = report; checkedFor = applied; lastCheck = Date.now();
|
|
1057
|
+
} catch (error) { log(`tailscale serve health check failed: ${error}`); }
|
|
1058
|
+
finally { checking = false; }
|
|
1059
|
+
}, 2000);
|
|
1044
1060
|
}
|
package/serve.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolvePlaywrightCli,
|
|
18
18
|
readTokenRegistry,
|
|
19
19
|
readChromeProfileCache,
|
|
20
|
+
checkTailscaleServe,
|
|
20
21
|
} from "./rechrome.ts";
|
|
21
22
|
|
|
22
23
|
const TAILSCALE_BIN = process.env.TAILSCALE_BIN || "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
@@ -1041,4 +1042,19 @@ export async function serve() {
|
|
|
1041
1042
|
} finally { reloading = false; }
|
|
1042
1043
|
}, 1000);
|
|
1043
1044
|
log("Connection credentials remain in local configuration; listener changes reload automatically");
|
|
1045
|
+
// Keep Tailscale Serve routes on the listeners' current ports: right away, after every listener
|
|
1046
|
+
// change, and every few minutes (routes can be edited by hand). Each finding is logged once.
|
|
1047
|
+
let checkedFor = "", lastReport = "", lastCheck = 0, checking = false;
|
|
1048
|
+
setInterval(async () => {
|
|
1049
|
+
if (checking || (applied === checkedFor && Date.now() - lastCheck < 5 * 60_000)) return;
|
|
1050
|
+
checking = true;
|
|
1051
|
+
try {
|
|
1052
|
+
const listeners = JSON.parse(applied || "[]") as Listener[];
|
|
1053
|
+
const lines = await checkTailscaleServe(listeners);
|
|
1054
|
+
const report = lines.join("\n");
|
|
1055
|
+
if (report && report !== lastReport) for (const line of lines) log(line);
|
|
1056
|
+
lastReport = report; checkedFor = applied; lastCheck = Date.now();
|
|
1057
|
+
} catch (error) { log(`tailscale serve health check failed: ${error}`); }
|
|
1058
|
+
finally { checking = false; }
|
|
1059
|
+
}, 2000);
|
|
1044
1060
|
}
|