@iamken/cloudtunnel 0.10.4 → 0.11.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/README.md +48 -192
- package/dist/{chunk-ZIUYW2HQ.js → chunk-7XACBYL7.js} +5 -1
- package/dist/{chunk-ZIUYW2HQ.js.map → chunk-7XACBYL7.js.map} +1 -1
- package/dist/{chunk-IDTF7SUY.js → chunk-ABLZ7XRZ.js} +2 -2
- package/dist/{chunk-NB3OEY2P.js → chunk-OT3UCBBF.js} +2 -2
- package/dist/{dns-2F5SYQNX.js → dns-DKPXBRCH.js} +3 -3
- package/dist/index.js +224 -30
- package/dist/index.js.map +1 -1
- package/dist/zones-XNCXYCB4.js +11 -0
- package/package.json +1 -1
- package/dist/zones-PCUEPXHD.js +0 -11
- /package/dist/{chunk-IDTF7SUY.js.map → chunk-ABLZ7XRZ.js.map} +0 -0
- /package/dist/{chunk-NB3OEY2P.js.map → chunk-OT3UCBBF.js.map} +0 -0
- /package/dist/{dns-2F5SYQNX.js.map → dns-DKPXBRCH.js.map} +0 -0
- /package/dist/{zones-PCUEPXHD.js.map → zones-XNCXYCB4.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
listZones,
|
|
4
4
|
resolveZone
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OT3UCBBF.js";
|
|
6
6
|
import {
|
|
7
7
|
createCname,
|
|
8
8
|
deleteDnsRecord,
|
|
9
9
|
findCname,
|
|
10
10
|
isManagedDns
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ABLZ7XRZ.js";
|
|
12
12
|
import {
|
|
13
13
|
CliError,
|
|
14
14
|
DEFAULT_API_BASE,
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
say,
|
|
42
42
|
scanCacheFile,
|
|
43
43
|
selectOne
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-7XACBYL7.js";
|
|
45
45
|
|
|
46
46
|
// src/index.ts
|
|
47
47
|
import { Command } from "commander";
|
|
@@ -143,18 +143,22 @@ function buildUpArgs(p) {
|
|
|
143
143
|
"-d",
|
|
144
144
|
p.zone,
|
|
145
145
|
...p.proto === "https" ? ["--proto", "https"] : [],
|
|
146
|
+
"--keep-alive",
|
|
146
147
|
"-f",
|
|
147
148
|
"-y"
|
|
148
149
|
];
|
|
149
150
|
}
|
|
150
151
|
const spec = formatTunnelSpec({ subdomain: p.subdomain, port: p.port, host: p.host });
|
|
152
|
+
const proto = p.protocol ?? "http2";
|
|
151
153
|
return [
|
|
152
154
|
"up",
|
|
153
155
|
spec,
|
|
154
156
|
"-d",
|
|
155
157
|
p.zone,
|
|
156
158
|
...p.proto === "https" ? ["--proto", "https"] : [],
|
|
157
|
-
|
|
159
|
+
"--protocol",
|
|
160
|
+
proto,
|
|
161
|
+
"--keep-alive",
|
|
158
162
|
"-f",
|
|
159
163
|
"-y"
|
|
160
164
|
];
|
|
@@ -204,6 +208,7 @@ function buildUnit(d) {
|
|
|
204
208
|
`Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
|
|
205
209
|
"After=network-online.target",
|
|
206
210
|
"Wants=network-online.target",
|
|
211
|
+
"StartLimitIntervalSec=0",
|
|
207
212
|
"",
|
|
208
213
|
"[Service]",
|
|
209
214
|
"Type=simple",
|
|
@@ -211,7 +216,7 @@ function buildUnit(d) {
|
|
|
211
216
|
`Environment=HOME=${d.home}`,
|
|
212
217
|
`Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
213
218
|
`ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
|
|
214
|
-
"Restart=
|
|
219
|
+
"Restart=always",
|
|
215
220
|
"RestartSec=5",
|
|
216
221
|
"",
|
|
217
222
|
"[Install]",
|
|
@@ -381,7 +386,7 @@ function buildTaskXml(d) {
|
|
|
381
386
|
" <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
|
|
382
387
|
" <StartWhenAvailable>true</StartWhenAvailable>",
|
|
383
388
|
" <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
|
|
384
|
-
" <RestartOnFailure><Interval>PT1M</Interval><Count>
|
|
389
|
+
" <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>",
|
|
385
390
|
" <Enabled>true</Enabled>",
|
|
386
391
|
" </Settings>",
|
|
387
392
|
' <Actions Context="Author">',
|
|
@@ -937,6 +942,7 @@ function extractTgz(bytes) {
|
|
|
937
942
|
|
|
938
943
|
// src/core/up-runner.ts
|
|
939
944
|
import { join as join7 } from "path";
|
|
945
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
940
946
|
import * as clack2 from "@clack/prompts";
|
|
941
947
|
|
|
942
948
|
// src/connector/process.ts
|
|
@@ -1063,7 +1069,7 @@ async function reconcile() {
|
|
|
1063
1069
|
// src/connector/process.ts
|
|
1064
1070
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1065
1071
|
function startConnector(opts) {
|
|
1066
|
-
const args = ["tunnel", "run"];
|
|
1072
|
+
const args = ["tunnel", "run", "--metrics", "127.0.0.1:0"];
|
|
1067
1073
|
if (opts.protocol) args.push("--protocol", opts.protocol);
|
|
1068
1074
|
const env = { ...process.env, TUNNEL_TOKEN: opts.token };
|
|
1069
1075
|
const fd = openSync(opts.logFile, "a", 384);
|
|
@@ -1171,6 +1177,34 @@ async function waitHealthy(cf, tunnelId, opts = {}) {
|
|
|
1171
1177
|
}
|
|
1172
1178
|
return opts.signal?.aborted ? "dead" : "provisioning";
|
|
1173
1179
|
}
|
|
1180
|
+
function extractMetricsPort(log) {
|
|
1181
|
+
const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
|
|
1182
|
+
if (!matches.length) return void 0;
|
|
1183
|
+
const last = matches[matches.length - 1];
|
|
1184
|
+
const port = Number(last?.[1]);
|
|
1185
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
1186
|
+
}
|
|
1187
|
+
async function checkLocalReady(port, opts = {}) {
|
|
1188
|
+
const controller = new AbortController();
|
|
1189
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
|
|
1190
|
+
try {
|
|
1191
|
+
const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
|
|
1192
|
+
if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
|
|
1193
|
+
const data = await res.json().catch(() => ({}));
|
|
1194
|
+
const readyConnections = Number(data.readyConnections) || 0;
|
|
1195
|
+
return { healthy: readyConnections > 0, readyConnections, status: res.status };
|
|
1196
|
+
} catch {
|
|
1197
|
+
return { healthy: false, readyConnections: 0 };
|
|
1198
|
+
} finally {
|
|
1199
|
+
clearTimeout(timer);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async function probeEntryState(entry, gone) {
|
|
1203
|
+
if (gone || entry.state !== "running" || !entry.pid) return "down";
|
|
1204
|
+
if (!entry.metricsPort) return "up";
|
|
1205
|
+
const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
|
|
1206
|
+
return ready.healthy ? "up" : "reconnecting";
|
|
1207
|
+
}
|
|
1174
1208
|
|
|
1175
1209
|
// src/core/orchestrator-create.ts
|
|
1176
1210
|
import { randomInt as randomInt2 } from "crypto";
|
|
@@ -1432,8 +1466,8 @@ async function resolveRemoteTarget(cf, target) {
|
|
|
1432
1466
|
}
|
|
1433
1467
|
const tunnel = matches[0];
|
|
1434
1468
|
if (!tunnel) return null;
|
|
1435
|
-
const { listCargoCnames } = await import("./dns-
|
|
1436
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1469
|
+
const { listCargoCnames } = await import("./dns-DKPXBRCH.js");
|
|
1470
|
+
const { listZones: listZones3 } = await import("./zones-XNCXYCB4.js");
|
|
1437
1471
|
for (const zone of await listZones3(cf.token)) {
|
|
1438
1472
|
const rec = (await listCargoCnames(cf.token, zone.id)).find((r) => tunnelIdFromCname2(r.content) === tunnel.id);
|
|
1439
1473
|
if (rec) return { tunnel, fqdn: rec.name };
|
|
@@ -1454,24 +1488,25 @@ async function removeTunnelById(cf, tunnel, opts = {}) {
|
|
|
1454
1488
|
async function listAll(cf, opts = {}) {
|
|
1455
1489
|
const entries = await reconcile();
|
|
1456
1490
|
const tunnels = new Map((await listTunnels(cf)).map((t) => [t.id, t]));
|
|
1457
|
-
const rows = entries.map((e) => {
|
|
1491
|
+
const rows = await Promise.all(entries.map(async (e) => {
|
|
1458
1492
|
const fqdn = entryFqdn(e);
|
|
1459
1493
|
const gone = e.tunnelId ? !tunnels.has(e.tunnelId) : false;
|
|
1460
1494
|
const svc = serviceState(fqdn);
|
|
1495
|
+
const state4 = await probeEntryState(e, gone);
|
|
1461
1496
|
return {
|
|
1462
1497
|
num: e.index ? String(e.index) : "-",
|
|
1463
1498
|
url: `https://${fqdn}`,
|
|
1464
1499
|
target: serviceUrl(e.proto, e.host ?? "localhost", e.port),
|
|
1465
1500
|
protocol: e.protocol ?? "auto",
|
|
1466
|
-
state:
|
|
1501
|
+
state: state4,
|
|
1467
1502
|
service: svc === "none" ? "-" : svc,
|
|
1468
1503
|
pid: e.state === "running" && e.pid ? String(e.pid) : "-",
|
|
1469
1504
|
managed: true
|
|
1470
1505
|
};
|
|
1471
|
-
});
|
|
1506
|
+
}));
|
|
1472
1507
|
if (opts.all) {
|
|
1473
|
-
const { listCargoCnames } = await import("./dns-
|
|
1474
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1508
|
+
const { listCargoCnames } = await import("./dns-DKPXBRCH.js");
|
|
1509
|
+
const { listZones: listZones3 } = await import("./zones-XNCXYCB4.js");
|
|
1475
1510
|
const tracked = new Set(entries.map(entryFqdn));
|
|
1476
1511
|
const unmanaged = [];
|
|
1477
1512
|
for (const zone of await listZones3(cf.token)) {
|
|
@@ -1492,9 +1527,141 @@ async function listAll(cf, opts = {}) {
|
|
|
1492
1527
|
return rows;
|
|
1493
1528
|
}
|
|
1494
1529
|
|
|
1530
|
+
// src/core/watchdog.ts
|
|
1531
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
1532
|
+
var activeWatchdogs = /* @__PURE__ */ new Map();
|
|
1533
|
+
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1534
|
+
function resolveMetricsPort(logFile, fallback) {
|
|
1535
|
+
if (existsSync6(logFile)) {
|
|
1536
|
+
try {
|
|
1537
|
+
const port = extractMetricsPort(readFileSync5(logFile, "utf8"));
|
|
1538
|
+
if (port) return port;
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return fallback;
|
|
1543
|
+
}
|
|
1544
|
+
async function verifyRemoteTunnel(cf, tunnelId) {
|
|
1545
|
+
try {
|
|
1546
|
+
await getTunnel(cf, tunnelId);
|
|
1547
|
+
return true;
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
if (err instanceof CliError && err.status === 404) return false;
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function tier1Restart(ctx, state4) {
|
|
1554
|
+
state4.lastRestartAt = Date.now();
|
|
1555
|
+
state4.tier1Restarts++;
|
|
1556
|
+
say.warn(`[watchdog] Health check failed for ${ctx.fqdn} -> restarting connector (Tier 1)...`);
|
|
1557
|
+
await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
|
|
1558
|
+
const conn = startConnector({
|
|
1559
|
+
bin: ctx.bin,
|
|
1560
|
+
token: ctx.token,
|
|
1561
|
+
detach: false,
|
|
1562
|
+
logFile: ctx.logFile,
|
|
1563
|
+
protocol: ctx.protocol,
|
|
1564
|
+
onExit: () => {
|
|
1565
|
+
void onConnectorExited(ctx.fqdn);
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
ctx.pid = conn.pid;
|
|
1569
|
+
await sleep3(1e3);
|
|
1570
|
+
ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1571
|
+
await patchEntry(ctx.fqdn, { pid: conn.pid, metricsPort: ctx.metricsPort, bootId: currentBootId() });
|
|
1572
|
+
state4.consecutiveFailures = 0;
|
|
1573
|
+
}
|
|
1574
|
+
async function tier2Reprovision(ctx, state4) {
|
|
1575
|
+
state4.lastRestartAt = Date.now();
|
|
1576
|
+
state4.tier2Restarts++;
|
|
1577
|
+
say.warn(`[watchdog] Tunnel missing or unrecoverable for ${ctx.fqdn} -> re-provisioning (Tier 2)...`);
|
|
1578
|
+
await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
|
|
1579
|
+
const result = await createTunnelSubdomain(ctx.cf, { ...ctx.item, force: true, yes: true });
|
|
1580
|
+
ctx.tunnelId = result.tunnelId;
|
|
1581
|
+
ctx.token = result.token;
|
|
1582
|
+
const conn = startConnector({
|
|
1583
|
+
bin: ctx.bin,
|
|
1584
|
+
token: ctx.token,
|
|
1585
|
+
detach: false,
|
|
1586
|
+
logFile: ctx.logFile,
|
|
1587
|
+
protocol: ctx.protocol,
|
|
1588
|
+
onExit: () => {
|
|
1589
|
+
void onConnectorExited(ctx.fqdn);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
ctx.pid = conn.pid;
|
|
1593
|
+
await sleep3(1e3);
|
|
1594
|
+
ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1595
|
+
await patchEntry(ctx.fqdn, { pid: conn.pid, metricsPort: ctx.metricsPort, bootId: currentBootId(), tunnelId: ctx.tunnelId });
|
|
1596
|
+
state4.consecutiveFailures = 0;
|
|
1597
|
+
state4.tier1Restarts = 0;
|
|
1598
|
+
}
|
|
1599
|
+
async function onConnectorExited(fqdn) {
|
|
1600
|
+
const item = activeWatchdogs.get(fqdn);
|
|
1601
|
+
if (!item) return;
|
|
1602
|
+
const { ctx, state: state4 } = item;
|
|
1603
|
+
if (Date.now() - state4.lastRestartAt < 5e3) return;
|
|
1604
|
+
await tier1Restart(ctx, state4);
|
|
1605
|
+
}
|
|
1606
|
+
function startTunnelWatchdog(ctx, opts = {}) {
|
|
1607
|
+
stopTunnelWatchdog(ctx.fqdn);
|
|
1608
|
+
const intervalMs = opts.intervalMs ?? 15e3;
|
|
1609
|
+
const cooldownMs = opts.cooldownMs ?? 5e3;
|
|
1610
|
+
const state4 = { consecutiveFailures: 0, tier1Restarts: 0, tier2Restarts: 0, lastRestartAt: 0 };
|
|
1611
|
+
const check = async () => {
|
|
1612
|
+
if (!ctx.metricsPort) ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1613
|
+
let healthy = false;
|
|
1614
|
+
if (ctx.metricsPort) {
|
|
1615
|
+
const res = await checkLocalReady(ctx.metricsPort);
|
|
1616
|
+
healthy = res.healthy;
|
|
1617
|
+
}
|
|
1618
|
+
if (healthy) {
|
|
1619
|
+
state4.consecutiveFailures = 0;
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
state4.consecutiveFailures++;
|
|
1623
|
+
if (state4.consecutiveFailures >= 3 && Date.now() - state4.lastRestartAt >= cooldownMs) {
|
|
1624
|
+
const remoteExists = await verifyRemoteTunnel(ctx.cf, ctx.tunnelId);
|
|
1625
|
+
if (!remoteExists || state4.tier1Restarts >= 3) {
|
|
1626
|
+
await tier2Reprovision(ctx, state4);
|
|
1627
|
+
} else {
|
|
1628
|
+
await tier1Restart(ctx, state4);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
const timer = setInterval(() => {
|
|
1633
|
+
void check();
|
|
1634
|
+
}, intervalMs);
|
|
1635
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1636
|
+
activeWatchdogs.set(ctx.fqdn, { timer, ctx, state: state4 });
|
|
1637
|
+
}
|
|
1638
|
+
function stopTunnelWatchdog(fqdn) {
|
|
1639
|
+
const item = activeWatchdogs.get(fqdn);
|
|
1640
|
+
if (item) {
|
|
1641
|
+
clearInterval(item.timer);
|
|
1642
|
+
activeWatchdogs.delete(fqdn);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function stopAllWatchdogs() {
|
|
1646
|
+
for (const [fqdn] of activeWatchdogs) stopTunnelWatchdog(fqdn);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1495
1649
|
// src/core/up-runner.ts
|
|
1496
|
-
|
|
1497
|
-
|
|
1650
|
+
var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1651
|
+
function logFileFor(fqdn) {
|
|
1652
|
+
return join7(logDir, `${serviceSlug(fqdn)}.log`);
|
|
1653
|
+
}
|
|
1654
|
+
async function createWithBootRetry(cf, item, keepAlive) {
|
|
1655
|
+
const deadline = Date.now() + (keepAlive ? 3e4 : 0);
|
|
1656
|
+
for (let attempt = 0; ; attempt++) {
|
|
1657
|
+
try {
|
|
1658
|
+
return await createTunnelSubdomain(cf, item);
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
if (!keepAlive || Date.now() >= deadline) throw err;
|
|
1661
|
+
say.debug(`[boot-retry] Waiting for network to create ${item.name ?? "tunnel"}...`);
|
|
1662
|
+
await sleep4(Math.min(2 ** attempt * 1e3, 5e3));
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1498
1665
|
}
|
|
1499
1666
|
async function startTunnels(cf, bin, items, opts = {}) {
|
|
1500
1667
|
const started = [];
|
|
@@ -1502,14 +1669,19 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1502
1669
|
const teardownAll = async (code) => {
|
|
1503
1670
|
if (tornDown) return;
|
|
1504
1671
|
tornDown = true;
|
|
1672
|
+
stopAllWatchdogs();
|
|
1505
1673
|
try {
|
|
1506
1674
|
for (const s of started) {
|
|
1507
|
-
|
|
1508
|
-
await
|
|
1509
|
-
}
|
|
1675
|
+
if (opts.keepAlive) {
|
|
1676
|
+
await stopConnector({ pid: s.pid, bootId: currentBootId() });
|
|
1677
|
+
} else {
|
|
1678
|
+
try {
|
|
1679
|
+
await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1510
1682
|
}
|
|
1511
1683
|
}
|
|
1512
|
-
if (process.stdout.isTTY) clack2.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
|
|
1684
|
+
if (process.stdout.isTTY && !opts.keepAlive) clack2.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
|
|
1513
1685
|
} catch (err) {
|
|
1514
1686
|
reportError(err);
|
|
1515
1687
|
} finally {
|
|
@@ -1520,9 +1692,9 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1520
1692
|
spin.start(items.length > 1 ? "Creating tunnels\u2026" : "Creating tunnel\u2026");
|
|
1521
1693
|
for (const item of items) {
|
|
1522
1694
|
spin.message(`Creating ${item.name ?? "tunnel"} (:${item.port})\u2026`);
|
|
1523
|
-
const result = await
|
|
1695
|
+
const result = await createWithBootRetry(cf, item, opts.keepAlive);
|
|
1524
1696
|
const fqdn = result.host.hostname;
|
|
1525
|
-
const logFile = logFileFor(
|
|
1697
|
+
const logFile = logFileFor(fqdn);
|
|
1526
1698
|
const conn = startConnector({
|
|
1527
1699
|
bin,
|
|
1528
1700
|
token: result.token,
|
|
@@ -1530,13 +1702,21 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1530
1702
|
logFile,
|
|
1531
1703
|
protocol: opts.protocol,
|
|
1532
1704
|
onExit: opts.detach ? void 0 : (code) => {
|
|
1533
|
-
if (!tornDown) {
|
|
1705
|
+
if (!tornDown && !opts.keepAlive) {
|
|
1534
1706
|
say.warn(`Connector for ${fqdn} exited.`);
|
|
1535
1707
|
void teardownAll(code ?? 1);
|
|
1536
1708
|
}
|
|
1537
1709
|
}
|
|
1538
1710
|
});
|
|
1539
|
-
await
|
|
1711
|
+
await sleep4(600);
|
|
1712
|
+
let metricsPort;
|
|
1713
|
+
if (existsSync7(logFile)) {
|
|
1714
|
+
try {
|
|
1715
|
+
metricsPort = extractMetricsPort(readFileSync6(logFile, "utf8"));
|
|
1716
|
+
} catch {
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile, metricsPort, protocol: opts.protocol });
|
|
1540
1720
|
started.push({
|
|
1541
1721
|
fqdn,
|
|
1542
1722
|
subdomain: result.host.subdomain,
|
|
@@ -1544,6 +1724,20 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1544
1724
|
target: serviceUrl(item.proto, item.host ?? "localhost", item.port),
|
|
1545
1725
|
pid: conn.pid
|
|
1546
1726
|
});
|
|
1727
|
+
if (!opts.detach) {
|
|
1728
|
+
startTunnelWatchdog({
|
|
1729
|
+
cf,
|
|
1730
|
+
bin,
|
|
1731
|
+
fqdn,
|
|
1732
|
+
tunnelId: result.tunnelId,
|
|
1733
|
+
token: result.token,
|
|
1734
|
+
logFile,
|
|
1735
|
+
item,
|
|
1736
|
+
pid: conn.pid,
|
|
1737
|
+
metricsPort,
|
|
1738
|
+
protocol: opts.protocol
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1547
1741
|
}
|
|
1548
1742
|
if (opts.detach) {
|
|
1549
1743
|
spin.stop(`${started.length} tunnel(s) started in the background`);
|
|
@@ -1561,7 +1755,7 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1561
1755
|
spin.stop(`${started.length} tunnel(s) started`);
|
|
1562
1756
|
const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
|
|
1563
1757
|
clack2.note(lines.join("\n"), `${live}/${started.length} live`);
|
|
1564
|
-
say.dim("Ctrl-C stops and releases them.");
|
|
1758
|
+
say.dim(opts.keepAlive ? "Service running with auto-heal watchdog." : "Ctrl-C stops and releases them.");
|
|
1565
1759
|
}
|
|
1566
1760
|
|
|
1567
1761
|
// src/core/resolve-domain.ts
|
|
@@ -1642,7 +1836,7 @@ async function runUp(specArgs, opts) {
|
|
|
1642
1836
|
await registerServices(cf, items, domain, opts.proto, protocol);
|
|
1643
1837
|
return;
|
|
1644
1838
|
}
|
|
1645
|
-
await startTunnels(cf, bin, items, { detach: opts.detach, protocol });
|
|
1839
|
+
await startTunnels(cf, bin, items, { detach: opts.detach, protocol, keepAlive: opts.keepAlive });
|
|
1646
1840
|
}
|
|
1647
1841
|
var SERVICE_UP_TIMEOUT_MS = 2e4;
|
|
1648
1842
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1688,7 +1882,7 @@ async function waitServicesUp(fqdns, timeoutMs) {
|
|
|
1688
1882
|
return [...pending];
|
|
1689
1883
|
}
|
|
1690
1884
|
function registerUp(program) {
|
|
1691
|
-
program.command("up", { isDefault: true }).argument("[specs...]", "tunnels to start: [subdomain:]port[@host] (e.g. api:8080 web:8081@localhost)").description("Start one or more tunnels (also: `cloudtunnel 8080`)").option("-d, --domain <domain>", "domain for the subdomains (prompted from a list if unset)").option("--proto <proto>", "local service protocol: http | https", "http").option("--protocol <proto>", "cloudflared edge transport: auto | http2 | quic (http2 for UDP-hostile networks)").option("--detach", "run the connectors in the background").option("--service", "register each subdomain as a boot service (Linux systemd \xB7 macOS launchd \xB7 Windows Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt; don't ask before replacing an existing record").action((specs, opts) => runUp(specs, opts));
|
|
1885
|
+
program.command("up", { isDefault: true }).argument("[specs...]", "tunnels to start: [subdomain:]port[@host] (e.g. api:8080 web:8081@localhost)").description("Start one or more tunnels (also: `cloudtunnel 8080`)").option("-d, --domain <domain>", "domain for the subdomains (prompted from a list if unset)").option("--proto <proto>", "local service protocol: http | https", "http").option("--protocol <proto>", "cloudflared edge transport: auto | http2 | quic (http2 for UDP-hostile networks)").option("--detach", "run the connectors in the background").option("--keep-alive", "supervised mode with auto-heal watchdog").option("--service", "register each subdomain as a boot service (Linux systemd \xB7 macOS launchd \xB7 Windows Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt; don't ask before replacing an existing record").action((specs, opts) => runUp(specs, opts));
|
|
1692
1886
|
}
|
|
1693
1887
|
|
|
1694
1888
|
// src/commands/ls.ts
|
|
@@ -1763,9 +1957,9 @@ function registerDelete(program) {
|
|
|
1763
1957
|
}
|
|
1764
1958
|
|
|
1765
1959
|
// src/commands/logs.ts
|
|
1766
|
-
import { closeSync, existsSync as
|
|
1960
|
+
import { closeSync, existsSync as existsSync8, openSync as openSync2, readFileSync as readFileSync7, readSync, statSync, watch } from "fs";
|
|
1767
1961
|
function printTail(file, n) {
|
|
1768
|
-
const lines =
|
|
1962
|
+
const lines = readFileSync7(file, "utf8").split("\n");
|
|
1769
1963
|
const tail = lines.slice(-n).join("\n");
|
|
1770
1964
|
process.stdout.write(tail.endsWith("\n") ? tail : `${tail}
|
|
1771
1965
|
`);
|
|
@@ -1797,7 +1991,7 @@ function follow(file, fromPos) {
|
|
|
1797
1991
|
function registerLogs(program) {
|
|
1798
1992
|
program.command("logs").argument("<target>", "subdomain name / hostname / id / #").description("Show the connector log for a subdomain (use -f to follow)").option("-f, --follow", "keep printing new log lines (like tail -f)").option("-n, --lines <n>", "number of lines to show", "50").action((name, opts) => {
|
|
1799
1993
|
const { fqdn, entry } = resolveTarget(name);
|
|
1800
|
-
if (!entry?.logFile || !
|
|
1994
|
+
if (!entry?.logFile || !existsSync8(entry.logFile)) {
|
|
1801
1995
|
throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
|
|
1802
1996
|
}
|
|
1803
1997
|
const n = Math.max(1, Number(opts.lines) || 50);
|