@iamken/cloudtunnel 0.11.5 → 0.12.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 +5 -1
- package/dist/{chunk-OT3UCBBF.js → chunk-J6FFC5ZO.js} +2 -2
- package/dist/{chunk-FILVDOPL.js → chunk-XKPTCMTJ.js} +2 -2
- package/dist/{chunk-7XACBYL7.js → chunk-ZIMTYVHN.js} +12 -4
- package/dist/chunk-ZIMTYVHN.js.map +1 -0
- package/dist/{dns-BNDEFUCM.js → dns-VUPKRH46.js} +3 -3
- package/dist/index.js +556 -105
- package/dist/index.js.map +1 -1
- package/dist/zones-CF4XFFZZ.js +11 -0
- package/package.json +1 -1
- package/dist/chunk-7XACBYL7.js.map +0 -1
- package/dist/zones-XNCXYCB4.js +0 -11
- /package/dist/{chunk-OT3UCBBF.js.map → chunk-J6FFC5ZO.js.map} +0 -0
- /package/dist/{chunk-FILVDOPL.js.map → chunk-XKPTCMTJ.js.map} +0 -0
- /package/dist/{dns-BNDEFUCM.js.map → dns-VUPKRH46.js.map} +0 -0
- /package/dist/{zones-XNCXYCB4.js.map → zones-CF4XFFZZ.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
listZones,
|
|
4
4
|
resolveZone
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-J6FFC5ZO.js";
|
|
6
6
|
import {
|
|
7
7
|
createCname,
|
|
8
8
|
deleteDnsRecord,
|
|
9
9
|
findCname,
|
|
10
10
|
findDnsRecords,
|
|
11
11
|
isManagedDns
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XKPTCMTJ.js";
|
|
13
13
|
import {
|
|
14
14
|
CliError,
|
|
15
15
|
DEFAULT_API_BASE,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
binDir,
|
|
19
19
|
cfPaginate,
|
|
20
20
|
cfRequest,
|
|
21
|
+
configDir,
|
|
21
22
|
configFile,
|
|
22
23
|
confirm,
|
|
23
24
|
dim,
|
|
@@ -42,7 +43,7 @@ import {
|
|
|
42
43
|
say,
|
|
43
44
|
scanCacheFile,
|
|
44
45
|
selectOne
|
|
45
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-ZIMTYVHN.js";
|
|
46
47
|
|
|
47
48
|
// src/index.ts
|
|
48
49
|
import { Command } from "commander";
|
|
@@ -50,7 +51,7 @@ import { createRequire } from "module";
|
|
|
50
51
|
import pc2 from "picocolors";
|
|
51
52
|
|
|
52
53
|
// src/config/legacy-migrate.ts
|
|
53
|
-
import { existsSync as existsSync3, readFileSync, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
54
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
54
55
|
|
|
55
56
|
// src/core/service.ts
|
|
56
57
|
import { join as join5 } from "path";
|
|
@@ -134,6 +135,14 @@ function formatTunnelSpec(s) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
// src/core/service-exec.ts
|
|
138
|
+
var SERVICE_SCHEMA = 2;
|
|
139
|
+
var SCHEMA_RE = /cloudtunnel-schema:\s*(\d+)/;
|
|
140
|
+
var schemaMarker = `cloudtunnel-schema: ${SERVICE_SCHEMA}`;
|
|
141
|
+
function schemaOf(text2) {
|
|
142
|
+
const m = SCHEMA_RE.exec(text2);
|
|
143
|
+
return m ? Number(m[1]) : 1;
|
|
144
|
+
}
|
|
145
|
+
var unxml = (s) => s.replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
137
146
|
var fqdnFor = (subdomain, zone) => subdomain === "@" ? zone : `${subdomain}.${zone}`;
|
|
138
147
|
var serviceSlug = (fqdn) => fqdn.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
139
148
|
function buildUpArgs(p) {
|
|
@@ -192,19 +201,25 @@ __export(service_systemd_exports, {
|
|
|
192
201
|
install: () => install,
|
|
193
202
|
label: () => label,
|
|
194
203
|
legacyUnitExists: () => legacyUnitExists,
|
|
204
|
+
listInstalled: () => listInstalled,
|
|
205
|
+
parseUnit: () => parseUnit,
|
|
206
|
+
prepareChanges: () => prepareChanges,
|
|
195
207
|
removeLegacyUnit: () => removeLegacyUnit,
|
|
196
208
|
state: () => state,
|
|
197
|
-
uninstall: () => uninstall
|
|
209
|
+
uninstall: () => uninstall,
|
|
210
|
+
uninstallLabel: () => uninstallLabel
|
|
198
211
|
});
|
|
199
212
|
import { execFileSync } from "child_process";
|
|
200
|
-
import { existsSync, writeFileSync } from "fs";
|
|
213
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
201
214
|
import { tmpdir } from "os";
|
|
202
215
|
import { dirname, join as join2 } from "path";
|
|
216
|
+
var UNIT_DIR = "/etc/systemd/system";
|
|
203
217
|
var label = (fqdn) => `cloudtunnel-${serviceSlug(fqdn)}.service`;
|
|
204
|
-
var unitPath = (fqdn) =>
|
|
218
|
+
var unitPath = (fqdn) => `${UNIT_DIR}/${label(fqdn)}`;
|
|
205
219
|
function buildUnit(d) {
|
|
206
220
|
const nodeBin = dirname(d.nodePath);
|
|
207
221
|
return [
|
|
222
|
+
`# ${schemaMarker}`,
|
|
208
223
|
"[Unit]",
|
|
209
224
|
`Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
|
|
210
225
|
"After=network-online.target",
|
|
@@ -219,6 +234,8 @@ function buildUnit(d) {
|
|
|
219
234
|
`ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
|
|
220
235
|
"Restart=always",
|
|
221
236
|
"RestartSec=5",
|
|
237
|
+
"KillMode=mixed",
|
|
238
|
+
"TimeoutStopSec=15",
|
|
222
239
|
"",
|
|
223
240
|
"[Install]",
|
|
224
241
|
"WantedBy=multi-user.target",
|
|
@@ -256,13 +273,40 @@ function install(d) {
|
|
|
256
273
|
privileged(["systemctl", "restart", name]);
|
|
257
274
|
}
|
|
258
275
|
function uninstall(fqdn) {
|
|
276
|
+
uninstallLabel(label(fqdn));
|
|
277
|
+
}
|
|
278
|
+
function uninstallLabel(name) {
|
|
259
279
|
try {
|
|
260
|
-
privileged(["systemctl", "disable", "--now",
|
|
280
|
+
privileged(["systemctl", "disable", "--now", name]);
|
|
261
281
|
} catch {
|
|
262
282
|
}
|
|
263
|
-
privileged(["rm", "-f",
|
|
283
|
+
privileged(["rm", "-f", `${UNIT_DIR}/${name}`]);
|
|
264
284
|
privileged(["systemctl", "daemon-reload"]);
|
|
265
285
|
}
|
|
286
|
+
function prepareChanges() {
|
|
287
|
+
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
288
|
+
if (!isRoot) execFileSync("sudo", ["-v"], { stdio: "inherit" });
|
|
289
|
+
}
|
|
290
|
+
function parseUnit(name, text2, running) {
|
|
291
|
+
const exec = /^ExecStart=(.+)$/m.exec(text2)?.[1]?.trim();
|
|
292
|
+
return { label: name, command: exec ? exec.split(/\s+/) : [], schema: schemaOf(text2), running };
|
|
293
|
+
}
|
|
294
|
+
function listInstalled() {
|
|
295
|
+
let files = [];
|
|
296
|
+
try {
|
|
297
|
+
files = readdirSync(UNIT_DIR).filter((f) => /^cloudtunnel-.+\.service$/.test(f));
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
return files.map((f) => {
|
|
302
|
+
let text2 = "";
|
|
303
|
+
try {
|
|
304
|
+
text2 = readFileSync(`${UNIT_DIR}/${f}`, "utf8");
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
return parseUnit(f, text2, ["active", "activating", "reloading"].includes(query(["is-active", f])));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
266
310
|
function state(fqdn) {
|
|
267
311
|
const name = label(fqdn);
|
|
268
312
|
if (query(["is-active", name]) === "active") return "active";
|
|
@@ -291,11 +335,15 @@ __export(service_launchd_exports, {
|
|
|
291
335
|
buildPlist: () => buildPlist,
|
|
292
336
|
install: () => install2,
|
|
293
337
|
label: () => label2,
|
|
338
|
+
listInstalled: () => listInstalled2,
|
|
339
|
+
parsePlist: () => parsePlist,
|
|
340
|
+
prepareChanges: () => prepareChanges2,
|
|
294
341
|
state: () => state2,
|
|
295
|
-
uninstall: () => uninstall2
|
|
342
|
+
uninstall: () => uninstall2,
|
|
343
|
+
uninstallLabel: () => uninstallLabel2
|
|
296
344
|
});
|
|
297
345
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
298
|
-
import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
346
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
299
347
|
import { dirname as dirname2, join as join3 } from "path";
|
|
300
348
|
import os2 from "os";
|
|
301
349
|
var label2 = (fqdn) => `com.cloudtunnel.${serviceSlug(fqdn)}`;
|
|
@@ -310,6 +358,7 @@ function buildPlist(d) {
|
|
|
310
358
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
311
359
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
312
360
|
'<plist version="1.0">',
|
|
361
|
+
`<!-- ${schemaMarker} -->`,
|
|
313
362
|
"<dict>",
|
|
314
363
|
` <key>Label</key><string>${xml(label2(d.fqdn))}</string>`,
|
|
315
364
|
" <key>ProgramArguments</key>",
|
|
@@ -318,6 +367,7 @@ function buildPlist(d) {
|
|
|
318
367
|
" </array>",
|
|
319
368
|
" <key>RunAtLoad</key><true/>",
|
|
320
369
|
" <key>KeepAlive</key><true/>",
|
|
370
|
+
" <key>ThrottleInterval</key><integer>10</integer>",
|
|
321
371
|
" <key>EnvironmentVariables</key>",
|
|
322
372
|
" <dict>",
|
|
323
373
|
` <key>PATH</key><string>${xml(path)}</string>`,
|
|
@@ -338,6 +388,11 @@ function launchctl(args) {
|
|
|
338
388
|
return out ? out.toString() : "";
|
|
339
389
|
}
|
|
340
390
|
}
|
|
391
|
+
function waitUnloaded(agentLabel) {
|
|
392
|
+
for (let i = 0; i < 30 && launchctl(["list", agentLabel]).trim(); i++) {
|
|
393
|
+
execFileSync2("sleep", ["0.5"]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
341
396
|
function assertSupported2() {
|
|
342
397
|
}
|
|
343
398
|
function install2(d) {
|
|
@@ -346,13 +401,41 @@ function install2(d) {
|
|
|
346
401
|
const plist = plistPath(d.fqdn);
|
|
347
402
|
writeFileSync2(plist, buildPlist(d), { mode: 420 });
|
|
348
403
|
launchctl(["unload", "-w", plist]);
|
|
404
|
+
waitUnloaded(label2(d.fqdn));
|
|
349
405
|
execFileSync2("launchctl", ["load", "-w", plist], { stdio: "inherit" });
|
|
350
406
|
}
|
|
351
407
|
function uninstall2(fqdn) {
|
|
352
|
-
|
|
408
|
+
uninstallLabel2(label2(fqdn));
|
|
409
|
+
}
|
|
410
|
+
function uninstallLabel2(agentLabel) {
|
|
411
|
+
const plist = join3(agentsDir(), `${agentLabel}.plist`);
|
|
353
412
|
launchctl(["unload", "-w", plist]);
|
|
354
413
|
rmSync(plist, { force: true });
|
|
355
414
|
}
|
|
415
|
+
function prepareChanges2() {
|
|
416
|
+
}
|
|
417
|
+
function parsePlist(agentLabel, text2, running) {
|
|
418
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text2)?.[1] ?? "";
|
|
419
|
+
const command = [...block.matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unxml(m[1] ?? ""));
|
|
420
|
+
return { label: agentLabel, command, schema: schemaOf(text2), running };
|
|
421
|
+
}
|
|
422
|
+
function listInstalled2() {
|
|
423
|
+
let files = [];
|
|
424
|
+
try {
|
|
425
|
+
files = readdirSync2(agentsDir()).filter((f) => /^com\.cloudtunnel\..+\.plist$/.test(f));
|
|
426
|
+
} catch {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
return files.map((f) => {
|
|
430
|
+
const agentLabel = f.replace(/\.plist$/, "");
|
|
431
|
+
let text2 = "";
|
|
432
|
+
try {
|
|
433
|
+
text2 = readFileSync2(join3(agentsDir(), f), "utf8");
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
return parsePlist(agentLabel, text2, /"PID"\s*=/.test(launchctl(["list", agentLabel])));
|
|
437
|
+
});
|
|
438
|
+
}
|
|
356
439
|
function state2(fqdn) {
|
|
357
440
|
const info = launchctl(["list", label2(fqdn)]);
|
|
358
441
|
if (/"PID"\s*=/.test(info)) return "active";
|
|
@@ -366,8 +449,12 @@ __export(service_windows_exports, {
|
|
|
366
449
|
buildTaskXml: () => buildTaskXml,
|
|
367
450
|
install: () => install3,
|
|
368
451
|
label: () => label3,
|
|
452
|
+
listInstalled: () => listInstalled3,
|
|
453
|
+
parseTaskXml: () => parseTaskXml,
|
|
454
|
+
prepareChanges: () => prepareChanges3,
|
|
369
455
|
state: () => state3,
|
|
370
|
-
uninstall: () => uninstall3
|
|
456
|
+
uninstall: () => uninstall3,
|
|
457
|
+
uninstallLabel: () => uninstallLabel3
|
|
371
458
|
});
|
|
372
459
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
373
460
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -380,7 +467,7 @@ function buildTaskXml(d) {
|
|
|
380
467
|
return [
|
|
381
468
|
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
382
469
|
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
383
|
-
` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel)</Description></RegistrationInfo>`,
|
|
470
|
+
` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel) [${schemaMarker}]</Description></RegistrationInfo>`,
|
|
384
471
|
` <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml2(d.user)}</UserId></LogonTrigger></Triggers>`,
|
|
385
472
|
` <Principals><Principal id="Author"><UserId>${xml2(d.user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>`,
|
|
386
473
|
" <Settings>",
|
|
@@ -418,7 +505,25 @@ function install3(d) {
|
|
|
418
505
|
schtasks(["/Run", "/TN", name]);
|
|
419
506
|
}
|
|
420
507
|
function uninstall3(fqdn) {
|
|
421
|
-
|
|
508
|
+
uninstallLabel3(label3(fqdn));
|
|
509
|
+
}
|
|
510
|
+
function uninstallLabel3(name) {
|
|
511
|
+
schtasks(["/End", "/TN", name]);
|
|
512
|
+
schtasks(["/Delete", "/TN", name, "/F"]);
|
|
513
|
+
}
|
|
514
|
+
function prepareChanges3() {
|
|
515
|
+
}
|
|
516
|
+
function splitArgs(args) {
|
|
517
|
+
return [...args.matchAll(/"([^"]*)"|(\S+)/g)].map((m) => m[1] ?? m[2] ?? "");
|
|
518
|
+
}
|
|
519
|
+
function parseTaskXml(name, xmlText, running) {
|
|
520
|
+
const command = unxml(/<Command>([\s\S]*?)<\/Command>/.exec(xmlText)?.[1] ?? "");
|
|
521
|
+
const args = unxml(/<Arguments>([\s\S]*?)<\/Arguments>/.exec(xmlText)?.[1] ?? "");
|
|
522
|
+
return { label: name, command: command ? [command, ...splitArgs(args)] : [], schema: schemaOf(xmlText), running };
|
|
523
|
+
}
|
|
524
|
+
function listInstalled3() {
|
|
525
|
+
const names = schtasks(["/Query", "/FO", "CSV", "/NH"]).split(/\r?\n/).map((line) => /^"([^"]+)"/.exec(line)?.[1]?.replace(/^\\/, "")).filter((n) => !!n && /^cloudtunnel\\/i.test(n));
|
|
526
|
+
return [...new Set(names)].map((name) => parseTaskXml(name, schtasks(["/Query", "/TN", name, "/XML"]), true));
|
|
422
527
|
}
|
|
423
528
|
function state3(fqdn) {
|
|
424
529
|
const out = schtasks(["/Query", "/TN", label3(fqdn), "/FO", "LIST"]);
|
|
@@ -465,6 +570,15 @@ function installServiceForSpec(params) {
|
|
|
465
570
|
function uninstallService(fqdn) {
|
|
466
571
|
pick()?.uninstall(fqdn);
|
|
467
572
|
}
|
|
573
|
+
function listInstalledServices() {
|
|
574
|
+
return pick()?.listInstalled() ?? [];
|
|
575
|
+
}
|
|
576
|
+
function uninstallServiceLabel(label4) {
|
|
577
|
+
pick()?.uninstallLabel(label4);
|
|
578
|
+
}
|
|
579
|
+
function prepareServiceChanges() {
|
|
580
|
+
pick()?.prepareChanges();
|
|
581
|
+
}
|
|
468
582
|
function serviceState(fqdn) {
|
|
469
583
|
return pick()?.state(fqdn) ?? "none";
|
|
470
584
|
}
|
|
@@ -493,7 +607,7 @@ async function migrateLegacyProfiles() {
|
|
|
493
607
|
if (!existsSync3(profilesFile) || existsSync3(skipMarker)) return;
|
|
494
608
|
let profiles;
|
|
495
609
|
try {
|
|
496
|
-
profiles = JSON.parse(
|
|
610
|
+
profiles = JSON.parse(readFileSync3(profilesFile, "utf8"));
|
|
497
611
|
} catch {
|
|
498
612
|
return;
|
|
499
613
|
}
|
|
@@ -847,7 +961,7 @@ async function ensureAuth() {
|
|
|
847
961
|
// src/connector/binary.ts
|
|
848
962
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
849
963
|
import { createHash } from "crypto";
|
|
850
|
-
import { chmodSync, existsSync as existsSync4, readFileSync as
|
|
964
|
+
import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
851
965
|
import { join as join6 } from "path";
|
|
852
966
|
import { gunzipSync } from "zlib";
|
|
853
967
|
var PINNED_VERSION = "2026.7.3";
|
|
@@ -878,7 +992,7 @@ function cachedPath() {
|
|
|
878
992
|
}
|
|
879
993
|
function isMusl() {
|
|
880
994
|
try {
|
|
881
|
-
return process.platform === "linux" &&
|
|
995
|
+
return process.platform === "linux" && readFileSync4("/usr/bin/ldd", "utf8").includes("musl");
|
|
882
996
|
} catch {
|
|
883
997
|
return false;
|
|
884
998
|
}
|
|
@@ -1043,16 +1157,17 @@ async function probeEntryState(entry, gone) {
|
|
|
1043
1157
|
}
|
|
1044
1158
|
|
|
1045
1159
|
// src/core/up-runner.ts
|
|
1046
|
-
import { join as
|
|
1047
|
-
import { readFileSync as
|
|
1160
|
+
import { join as join9 } from "path";
|
|
1161
|
+
import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
|
|
1048
1162
|
import * as clack2 from "@clack/prompts";
|
|
1049
1163
|
|
|
1050
1164
|
// src/connector/process.ts
|
|
1051
1165
|
import { execFileSync as execFileSync7, spawn as spawn2 } from "child_process";
|
|
1052
|
-
import { openSync } from "fs";
|
|
1166
|
+
import { openSync, renameSync as renameSync3, statSync } from "fs";
|
|
1167
|
+
import { createServer } from "net";
|
|
1053
1168
|
|
|
1054
1169
|
// src/connector/registry.ts
|
|
1055
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1170
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
1056
1171
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
1057
1172
|
import { readFile } from "fs/promises";
|
|
1058
1173
|
import os3 from "os";
|
|
@@ -1062,7 +1177,7 @@ function entryFqdn(e) {
|
|
|
1062
1177
|
}
|
|
1063
1178
|
function currentBootId() {
|
|
1064
1179
|
try {
|
|
1065
|
-
return
|
|
1180
|
+
return readFileSync5("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
1066
1181
|
} catch {
|
|
1067
1182
|
if (process.platform === "darwin") {
|
|
1068
1183
|
try {
|
|
@@ -1077,7 +1192,7 @@ function currentBootId() {
|
|
|
1077
1192
|
}
|
|
1078
1193
|
function readRegistry() {
|
|
1079
1194
|
try {
|
|
1080
|
-
return JSON.parse(
|
|
1195
|
+
return JSON.parse(readFileSync5(registryFile, "utf8"));
|
|
1081
1196
|
} catch {
|
|
1082
1197
|
return {};
|
|
1083
1198
|
}
|
|
@@ -1167,14 +1282,17 @@ async function isOurConnector(entry) {
|
|
|
1167
1282
|
}
|
|
1168
1283
|
return true;
|
|
1169
1284
|
}
|
|
1285
|
+
function markConnectorRunning(fqdn, patch) {
|
|
1286
|
+
return patchEntry(fqdn, { bootId: currentBootId(), ...patch, state: "running" });
|
|
1287
|
+
}
|
|
1170
1288
|
async function reconcile() {
|
|
1171
1289
|
const entries = listEntries();
|
|
1172
1290
|
for (const entry of entries) {
|
|
1173
|
-
if (entry.state === "running" && !await isOurConnector(entry)) {
|
|
1291
|
+
if (entry.state === "running" && entry.pid && !await isOurConnector(entry)) {
|
|
1174
1292
|
const fqdn = entryFqdn(entry);
|
|
1175
1293
|
await mutateRegistry((reg) => {
|
|
1176
1294
|
const e = reg[fqdn];
|
|
1177
|
-
if (e) {
|
|
1295
|
+
if (e && e.state === "running" && e.pid === entry.pid) {
|
|
1178
1296
|
e.state = "stopped";
|
|
1179
1297
|
delete e.pid;
|
|
1180
1298
|
}
|
|
@@ -1186,10 +1304,30 @@ async function reconcile() {
|
|
|
1186
1304
|
|
|
1187
1305
|
// src/connector/process.ts
|
|
1188
1306
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1307
|
+
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
1308
|
+
function pickFreePort() {
|
|
1309
|
+
return new Promise((resolve, reject) => {
|
|
1310
|
+
const srv = createServer();
|
|
1311
|
+
srv.unref();
|
|
1312
|
+
srv.on("error", reject);
|
|
1313
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
1314
|
+
const addr = srv.address();
|
|
1315
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1316
|
+
srv.close(() => port ? resolve(port) : reject(new Error("no free port")));
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
function rotateLog(logFile) {
|
|
1321
|
+
try {
|
|
1322
|
+
if (statSync(logFile).size > MAX_LOG_BYTES) renameSync3(logFile, `${logFile}.1`);
|
|
1323
|
+
} catch {
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1189
1326
|
function startConnector(opts) {
|
|
1190
|
-
const args = ["tunnel", "--metrics",
|
|
1327
|
+
const args = ["tunnel", "--metrics", `127.0.0.1:${opts.metricsPort ?? 0}`, "run"];
|
|
1191
1328
|
if (opts.protocol) args.push("--protocol", opts.protocol);
|
|
1192
1329
|
const env = { ...process.env, TUNNEL_TOKEN: opts.token };
|
|
1330
|
+
rotateLog(opts.logFile);
|
|
1193
1331
|
const fd = openSync(opts.logFile, "a", 384);
|
|
1194
1332
|
const child = spawn2(opts.bin, args, { env, detached: opts.detach, stdio: ["ignore", fd, fd] });
|
|
1195
1333
|
if (!child.pid) throw new CliError("Failed to start the cloudflared connector.");
|
|
@@ -1227,6 +1365,54 @@ async function stopConnector(entry) {
|
|
|
1227
1365
|
return true;
|
|
1228
1366
|
}
|
|
1229
1367
|
|
|
1368
|
+
// src/connector/hostname-lock.ts
|
|
1369
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1370
|
+
import { join as join7 } from "path";
|
|
1371
|
+
import lockfile2 from "proper-lockfile";
|
|
1372
|
+
var locksDir = join7(configDir, "locks");
|
|
1373
|
+
var STALE_MS = 15e3;
|
|
1374
|
+
var UPDATE_MS = 5e3;
|
|
1375
|
+
var lockPath = (fqdn) => join7(locksDir, serviceSlug(fqdn));
|
|
1376
|
+
function busyError(fqdn) {
|
|
1377
|
+
return new CliError(`${fqdn} is already being served by another cloudtunnel process.`, {
|
|
1378
|
+
hint: "a boot service may own it \u2014 see `cloudtunnel ls`; release it with `cloudtunnel delete <#>`"
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
async function acquireHostnameLock(fqdn, opts = {}) {
|
|
1382
|
+
mkdirSync2(locksDir, { recursive: true, mode: 448 });
|
|
1383
|
+
const waits = Math.ceil((opts.waitMs ?? 0) / 1e3);
|
|
1384
|
+
try {
|
|
1385
|
+
await takeLock(fqdn, waits);
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
if (err.code === "ELOCKED") throw busyError(fqdn);
|
|
1388
|
+
throw err;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function takeLock(fqdn, waits) {
|
|
1392
|
+
return lockfile2.lock(lockPath(fqdn), {
|
|
1393
|
+
realpath: false,
|
|
1394
|
+
stale: STALE_MS,
|
|
1395
|
+
update: UPDATE_MS,
|
|
1396
|
+
retries: waits > 0 ? { retries: waits, minTimeout: 1e3, maxTimeout: 1e3 } : 0,
|
|
1397
|
+
onCompromised: () => {
|
|
1398
|
+
say.debug(`[lock] ${fqdn} lock was compromised; re-taking it`);
|
|
1399
|
+
takeLock(fqdn, 0).catch((err) => {
|
|
1400
|
+
say.warn(`Lost ${fqdn} to another cloudtunnel process (${err.message}) \u2014 exiting.`);
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
async function assertHostnameFree(fqdn) {
|
|
1407
|
+
let held = false;
|
|
1408
|
+
try {
|
|
1409
|
+
held = await lockfile2.check(lockPath(fqdn), { realpath: false, stale: STALE_MS });
|
|
1410
|
+
} catch {
|
|
1411
|
+
held = false;
|
|
1412
|
+
}
|
|
1413
|
+
if (held) throw busyError(fqdn);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1230
1416
|
// src/core/orchestrator-create.ts
|
|
1231
1417
|
import { randomInt as randomInt2 } from "crypto";
|
|
1232
1418
|
|
|
@@ -1300,6 +1486,10 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1300
1486
|
const host = resolveHostSpec(opts, opts.defaultZone);
|
|
1301
1487
|
const zone = await resolveZone(cf.token, host.zone);
|
|
1302
1488
|
const existingList = await findDnsRecords(cf.token, zone.id, host.hostname);
|
|
1489
|
+
if (opts.reuse) {
|
|
1490
|
+
const reused = await tryReuseTunnel(cf, zone.id, host, opts, existingList);
|
|
1491
|
+
if (reused) return reused;
|
|
1492
|
+
}
|
|
1303
1493
|
if (existingList.length > 0) {
|
|
1304
1494
|
const isTunnelRecord = existingList.every((r) => r.content.endsWith(".cfargotunnel.com"));
|
|
1305
1495
|
if (!isTunnelRecord && !opts.force) {
|
|
@@ -1353,6 +1543,25 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1353
1543
|
throw err;
|
|
1354
1544
|
}
|
|
1355
1545
|
}
|
|
1546
|
+
async function tryReuseTunnel(cf, zoneId, host, opts, existing) {
|
|
1547
|
+
const prev = getEntry(host.hostname);
|
|
1548
|
+
const tunnelId = prev?.tunnelId;
|
|
1549
|
+
const record = existing[0];
|
|
1550
|
+
if (!prev || !tunnelId || existing.length !== 1 || record?.content !== `${tunnelId}.cfargotunnel.com`) return null;
|
|
1551
|
+
let tunnel;
|
|
1552
|
+
try {
|
|
1553
|
+
tunnel = await getTunnel(cf, tunnelId);
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
if (err instanceof CliError && err.status === 404) return null;
|
|
1556
|
+
throw err;
|
|
1557
|
+
}
|
|
1558
|
+
if (!isManagedTunnel(tunnel) || tunnel.deleted_at) return null;
|
|
1559
|
+
await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto, host: opts.host }));
|
|
1560
|
+
const token = await getTunnelToken(cf, tunnelId);
|
|
1561
|
+
await stopConnector(prev);
|
|
1562
|
+
await recordRunning(host, zoneId, tunnelId, record.id, opts);
|
|
1563
|
+
return { host, tunnelId, token };
|
|
1564
|
+
}
|
|
1356
1565
|
async function recordRunning(host, zoneId, tunnelId, dnsRecordId, opts) {
|
|
1357
1566
|
await upsertEntry(host.hostname, {
|
|
1358
1567
|
subdomain: host.subdomain,
|
|
@@ -1399,8 +1608,105 @@ async function rollback(cf, zoneId, tunnelId, dnsRecordId, hostname) {
|
|
|
1399
1608
|
return clean;
|
|
1400
1609
|
}
|
|
1401
1610
|
|
|
1611
|
+
// src/core/service-inventory.ts
|
|
1612
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1613
|
+
|
|
1614
|
+
// src/core/transport-protocol.ts
|
|
1615
|
+
function parseTransportProtocol(value) {
|
|
1616
|
+
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
1617
|
+
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/core/service-inventory.ts
|
|
1621
|
+
function flagValue(argv, flag) {
|
|
1622
|
+
const i = argv.indexOf(flag);
|
|
1623
|
+
return i >= 0 ? argv[i + 1] : void 0;
|
|
1624
|
+
}
|
|
1625
|
+
function parseServiceArgv(argv) {
|
|
1626
|
+
const zone = flagValue(argv, "-d");
|
|
1627
|
+
if (!zone) return void 0;
|
|
1628
|
+
const proto = flagValue(argv, "--proto") === "https" ? "https" : "http";
|
|
1629
|
+
try {
|
|
1630
|
+
if (argv[0] === "relay" && argv[1]) {
|
|
1631
|
+
return { command: "relay", subdomain: argv[1], port: 0, zone, proto };
|
|
1632
|
+
}
|
|
1633
|
+
if (argv[0] === "up" && argv[1]) {
|
|
1634
|
+
const spec = parseTunnelSpec(argv[1]);
|
|
1635
|
+
if (spec.subdomain === void 0) return void 0;
|
|
1636
|
+
const protocolFlag = flagValue(argv, "--protocol");
|
|
1637
|
+
return {
|
|
1638
|
+
subdomain: spec.subdomain,
|
|
1639
|
+
port: spec.port,
|
|
1640
|
+
host: spec.host,
|
|
1641
|
+
zone,
|
|
1642
|
+
proto,
|
|
1643
|
+
protocol: protocolFlag ? parseTransportProtocol(protocolFlag) : void 0
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
} catch {
|
|
1647
|
+
return void 0;
|
|
1648
|
+
}
|
|
1649
|
+
return void 0;
|
|
1650
|
+
}
|
|
1651
|
+
function classifyDefinition(def, canonicalLabel, exists = existsSync6) {
|
|
1652
|
+
const [nodePath, scriptPath, ...argv] = def.command;
|
|
1653
|
+
const params = parseServiceArgv(argv);
|
|
1654
|
+
if (!params) return void 0;
|
|
1655
|
+
const fqdn = fqdnFor(params.subdomain, params.zone);
|
|
1656
|
+
const canonical = def.label.toLowerCase() === canonicalLabel(fqdn).toLowerCase();
|
|
1657
|
+
let issue;
|
|
1658
|
+
if (!canonical) issue = "duplicate";
|
|
1659
|
+
else if (!nodePath || !scriptPath || !exists(nodePath) || !exists(scriptPath)) issue = "broken";
|
|
1660
|
+
else if (def.schema < SERVICE_SCHEMA || !argv.includes("--keep-alive")) issue = "stale";
|
|
1661
|
+
else if (!def.running) issue = "dead";
|
|
1662
|
+
return { label: def.label, fqdn, params, canonical, issue };
|
|
1663
|
+
}
|
|
1664
|
+
function inventoryServices() {
|
|
1665
|
+
return listInstalledServices().map((def) => classifyDefinition(def, serviceName)).filter((s) => !!s);
|
|
1666
|
+
}
|
|
1667
|
+
function planRepair(services, opts = {}) {
|
|
1668
|
+
const byFqdn = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const s of services) byFqdn.set(s.fqdn, [...byFqdn.get(s.fqdn) ?? [], s]);
|
|
1670
|
+
const actions = [];
|
|
1671
|
+
for (const [fqdn, group] of byFqdn) {
|
|
1672
|
+
const main2 = group.find((s) => s.canonical);
|
|
1673
|
+
const extras = group.filter((s) => !s.canonical);
|
|
1674
|
+
const action = { fqdn, remove: extras.map((s) => s.label) };
|
|
1675
|
+
if (!main2) action.reinstall = { params: extras[0].params, reason: "duplicate" };
|
|
1676
|
+
else if (main2.issue) action.reinstall = { params: main2.params, reason: main2.issue };
|
|
1677
|
+
else if (opts.restartRunning) action.reinstall = { params: main2.params, reason: "outdated" };
|
|
1678
|
+
if (action.reinstall || action.remove.length) actions.push(action);
|
|
1679
|
+
}
|
|
1680
|
+
return actions;
|
|
1681
|
+
}
|
|
1682
|
+
function runRepair(actions) {
|
|
1683
|
+
const result = { fixed: [], failed: [] };
|
|
1684
|
+
if (actions.length === 0) return result;
|
|
1685
|
+
prepareServiceChanges();
|
|
1686
|
+
for (const a of actions) {
|
|
1687
|
+
try {
|
|
1688
|
+
for (const label4 of a.remove) uninstallServiceLabel(label4);
|
|
1689
|
+
if (a.reinstall) installServiceForSpec(a.reinstall.params);
|
|
1690
|
+
result.fixed.push(a.fqdn);
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
result.failed.push({ fqdn: a.fqdn, error: err.message });
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return result;
|
|
1696
|
+
}
|
|
1697
|
+
function serviceIssuesByFqdn() {
|
|
1698
|
+
const map = /* @__PURE__ */ new Map();
|
|
1699
|
+
try {
|
|
1700
|
+
for (const a of planRepair(inventoryServices())) {
|
|
1701
|
+
map.set(a.fqdn, a.reinstall?.reason ?? "duplicate");
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
return map;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1402
1708
|
// src/core/unmanaged-scan-cache.ts
|
|
1403
|
-
import { readFileSync as
|
|
1709
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
1404
1710
|
function saveUnmanagedScan(rows) {
|
|
1405
1711
|
try {
|
|
1406
1712
|
ensureDirs();
|
|
@@ -1412,7 +1718,7 @@ function saveUnmanagedScan(rows) {
|
|
|
1412
1718
|
}
|
|
1413
1719
|
function lookupUnmanagedByIndex(num) {
|
|
1414
1720
|
try {
|
|
1415
|
-
const cache = JSON.parse(
|
|
1721
|
+
const cache = JSON.parse(readFileSync6(scanCacheFile, "utf8"));
|
|
1416
1722
|
return cache[String(num)];
|
|
1417
1723
|
} catch {
|
|
1418
1724
|
return void 0;
|
|
@@ -1496,8 +1802,8 @@ async function resolveRemoteTarget(cf, target) {
|
|
|
1496
1802
|
}
|
|
1497
1803
|
const tunnel = matches[0];
|
|
1498
1804
|
if (!tunnel) return null;
|
|
1499
|
-
const { listCargoCnames } = await import("./dns-
|
|
1500
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1805
|
+
const { listCargoCnames } = await import("./dns-VUPKRH46.js");
|
|
1806
|
+
const { listZones: listZones3 } = await import("./zones-CF4XFFZZ.js");
|
|
1501
1807
|
for (const zone of await listZones3(cf.token)) {
|
|
1502
1808
|
const rec = (await listCargoCnames(cf.token, zone.id)).find((r) => tunnelIdFromCname2(r.content) === tunnel.id);
|
|
1503
1809
|
if (rec) return { tunnel, fqdn: rec.name };
|
|
@@ -1518,25 +1824,29 @@ async function removeTunnelById(cf, tunnel, opts = {}) {
|
|
|
1518
1824
|
async function listAll(cf, opts = {}) {
|
|
1519
1825
|
const entries = await reconcile();
|
|
1520
1826
|
const tunnels = new Map((await listTunnels(cf)).map((t) => [t.id, t]));
|
|
1827
|
+
const issues = serviceIssuesByFqdn();
|
|
1521
1828
|
const rows = await Promise.all(entries.map(async (e) => {
|
|
1522
1829
|
const fqdn = entryFqdn(e);
|
|
1523
1830
|
const gone = e.tunnelId ? !tunnels.has(e.tunnelId) : false;
|
|
1524
1831
|
const svc = serviceState(fqdn);
|
|
1525
1832
|
const state4 = await probeEntryState(e, gone);
|
|
1833
|
+
const issue = issues.get(fqdn);
|
|
1834
|
+
const svcLabel = svc === "none" ? "-" : svc;
|
|
1526
1835
|
return {
|
|
1527
1836
|
num: e.index ? String(e.index) : "-",
|
|
1528
1837
|
url: `https://${fqdn}`,
|
|
1529
1838
|
target: serviceUrl(e.proto, e.host ?? "localhost", e.port),
|
|
1530
1839
|
protocol: e.protocol ?? "auto",
|
|
1531
1840
|
state: state4,
|
|
1532
|
-
service:
|
|
1841
|
+
service: issue ? `${svcLabel} (${issue})` : svcLabel,
|
|
1533
1842
|
pid: e.state === "running" && e.pid ? String(e.pid) : "-",
|
|
1534
|
-
managed: true
|
|
1843
|
+
managed: true,
|
|
1844
|
+
serviceIssue: issue
|
|
1535
1845
|
};
|
|
1536
1846
|
}));
|
|
1537
1847
|
if (opts.all) {
|
|
1538
|
-
const { listCargoCnames } = await import("./dns-
|
|
1539
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1848
|
+
const { listCargoCnames } = await import("./dns-VUPKRH46.js");
|
|
1849
|
+
const { listZones: listZones3 } = await import("./zones-CF4XFFZZ.js");
|
|
1540
1850
|
const tracked = new Set(entries.map(entryFqdn));
|
|
1541
1851
|
const unmanaged = [];
|
|
1542
1852
|
for (const zone of await listZones3(cf.token)) {
|
|
@@ -1558,19 +1868,27 @@ async function listAll(cf, opts = {}) {
|
|
|
1558
1868
|
}
|
|
1559
1869
|
|
|
1560
1870
|
// src/core/watchdog.ts
|
|
1561
|
-
import { readFileSync as
|
|
1871
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
1562
1872
|
var activeWatchdogs = /* @__PURE__ */ new Map();
|
|
1563
1873
|
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1874
|
+
var EXIT_RESTART_GAP_MS = 5e3;
|
|
1564
1875
|
function resolveMetricsPort(logFile, fallback) {
|
|
1565
|
-
if (
|
|
1876
|
+
if (existsSync7(logFile)) {
|
|
1566
1877
|
try {
|
|
1567
|
-
const port = extractMetricsPort(
|
|
1878
|
+
const port = extractMetricsPort(readFileSync7(logFile, "utf8"));
|
|
1568
1879
|
if (port) return port;
|
|
1569
1880
|
} catch {
|
|
1570
1881
|
}
|
|
1571
1882
|
}
|
|
1572
1883
|
return fallback;
|
|
1573
1884
|
}
|
|
1885
|
+
async function freshMetricsPort() {
|
|
1886
|
+
try {
|
|
1887
|
+
return await pickFreePort();
|
|
1888
|
+
} catch {
|
|
1889
|
+
return void 0;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1574
1892
|
async function verifyRemoteTunnel(cf, tunnelId) {
|
|
1575
1893
|
try {
|
|
1576
1894
|
await getTunnel(cf, tunnelId);
|
|
@@ -1580,25 +1898,39 @@ async function verifyRemoteTunnel(cf, tunnelId) {
|
|
|
1580
1898
|
return true;
|
|
1581
1899
|
}
|
|
1582
1900
|
}
|
|
1583
|
-
async function
|
|
1584
|
-
|
|
1585
|
-
state4.tier1Restarts++;
|
|
1586
|
-
say.warn(`[watchdog] Health check failed for ${ctx.fqdn} -> restarting connector (Tier 1)...`);
|
|
1587
|
-
await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
|
|
1901
|
+
async function launchConnector(ctx) {
|
|
1902
|
+
const metricsPort = await freshMetricsPort();
|
|
1588
1903
|
const conn = startConnector({
|
|
1589
1904
|
bin: ctx.bin,
|
|
1590
1905
|
token: ctx.token,
|
|
1591
1906
|
detach: false,
|
|
1592
1907
|
logFile: ctx.logFile,
|
|
1593
1908
|
protocol: ctx.protocol,
|
|
1909
|
+
metricsPort,
|
|
1594
1910
|
onExit: () => {
|
|
1595
|
-
void
|
|
1911
|
+
void notifyConnectorExited(ctx.fqdn);
|
|
1596
1912
|
}
|
|
1597
1913
|
});
|
|
1914
|
+
if (activeWatchdogs.get(ctx.fqdn)?.ctx !== ctx) {
|
|
1915
|
+
await stopConnector({ pid: conn.pid, bootId: currentBootId() });
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1598
1918
|
ctx.pid = conn.pid;
|
|
1599
1919
|
await sleep3(1e3);
|
|
1600
|
-
ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1601
|
-
await
|
|
1920
|
+
ctx.metricsPort = metricsPort ?? resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1921
|
+
await markConnectorRunning(ctx.fqdn, {
|
|
1922
|
+
pid: conn.pid,
|
|
1923
|
+
metricsPort: ctx.metricsPort,
|
|
1924
|
+
bootId: currentBootId(),
|
|
1925
|
+
tunnelId: ctx.tunnelId
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
async function tier1Restart(ctx, state4) {
|
|
1929
|
+
state4.lastRestartAt = Date.now();
|
|
1930
|
+
state4.tier1Restarts++;
|
|
1931
|
+
say.warn(`[watchdog] Health check failed for ${ctx.fqdn} -> restarting connector (Tier 1)...`);
|
|
1932
|
+
await stopConnector({ pid: ctx.pid, bootId: currentBootId() });
|
|
1933
|
+
await launchConnector(ctx);
|
|
1602
1934
|
state4.consecutiveFailures = 0;
|
|
1603
1935
|
}
|
|
1604
1936
|
async function tier2Reprovision(ctx, state4) {
|
|
@@ -1609,36 +1941,46 @@ async function tier2Reprovision(ctx, state4) {
|
|
|
1609
1941
|
const result = await createTunnelSubdomain(ctx.cf, { ...ctx.item, force: true, yes: true });
|
|
1610
1942
|
ctx.tunnelId = result.tunnelId;
|
|
1611
1943
|
ctx.token = result.token;
|
|
1612
|
-
|
|
1613
|
-
bin: ctx.bin,
|
|
1614
|
-
token: ctx.token,
|
|
1615
|
-
detach: false,
|
|
1616
|
-
logFile: ctx.logFile,
|
|
1617
|
-
protocol: ctx.protocol,
|
|
1618
|
-
onExit: () => {
|
|
1619
|
-
void onConnectorExited(ctx.fqdn);
|
|
1620
|
-
}
|
|
1621
|
-
});
|
|
1622
|
-
ctx.pid = conn.pid;
|
|
1623
|
-
await sleep3(1e3);
|
|
1624
|
-
ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1625
|
-
await patchEntry(ctx.fqdn, { pid: conn.pid, metricsPort: ctx.metricsPort, bootId: currentBootId(), tunnelId: ctx.tunnelId });
|
|
1944
|
+
await launchConnector(ctx);
|
|
1626
1945
|
state4.consecutiveFailures = 0;
|
|
1627
1946
|
state4.tier1Restarts = 0;
|
|
1628
1947
|
}
|
|
1629
|
-
async function
|
|
1948
|
+
async function guardedRestart(fqdn, run2) {
|
|
1949
|
+
const item = activeWatchdogs.get(fqdn);
|
|
1950
|
+
if (!item || item.state.restarting) return;
|
|
1951
|
+
item.state.restarting = true;
|
|
1952
|
+
try {
|
|
1953
|
+
await run2();
|
|
1954
|
+
} catch (err) {
|
|
1955
|
+
say.warn(`[watchdog] Recovery for ${fqdn} failed: ${err.message} \u2014 retrying on the next check`);
|
|
1956
|
+
} finally {
|
|
1957
|
+
item.state.restarting = false;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
async function notifyConnectorExited(fqdn) {
|
|
1630
1961
|
const item = activeWatchdogs.get(fqdn);
|
|
1631
1962
|
if (!item) return;
|
|
1632
1963
|
const { ctx, state: state4 } = item;
|
|
1633
|
-
if (Date.now() - state4.lastRestartAt <
|
|
1634
|
-
|
|
1964
|
+
if (state4.restarting || Date.now() - state4.lastRestartAt < EXIT_RESTART_GAP_MS) return;
|
|
1965
|
+
const run2 = state4.tier1Restarts >= 3 ? tier2Reprovision : tier1Restart;
|
|
1966
|
+
await guardedRestart(fqdn, () => run2(ctx, state4));
|
|
1635
1967
|
}
|
|
1636
1968
|
function startTunnelWatchdog(ctx, opts = {}) {
|
|
1637
1969
|
stopTunnelWatchdog(ctx.fqdn);
|
|
1638
1970
|
const intervalMs = opts.intervalMs ?? 15e3;
|
|
1639
1971
|
const cooldownMs = opts.cooldownMs ?? 5e3;
|
|
1640
|
-
const state4 = { consecutiveFailures: 0, tier1Restarts: 0, tier2Restarts: 0, lastRestartAt: 0 };
|
|
1972
|
+
const state4 = { consecutiveFailures: 0, tier1Restarts: 0, tier2Restarts: 0, lastRestartAt: 0, restarting: false };
|
|
1973
|
+
let checking = false;
|
|
1641
1974
|
const check = async () => {
|
|
1975
|
+
if (state4.restarting || checking) return;
|
|
1976
|
+
checking = true;
|
|
1977
|
+
try {
|
|
1978
|
+
await runCheck();
|
|
1979
|
+
} finally {
|
|
1980
|
+
checking = false;
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
const runCheck = async () => {
|
|
1642
1984
|
if (!ctx.metricsPort) ctx.metricsPort = resolveMetricsPort(ctx.logFile, ctx.metricsPort);
|
|
1643
1985
|
let healthy = false;
|
|
1644
1986
|
if (ctx.metricsPort) {
|
|
@@ -1647,22 +1989,19 @@ function startTunnelWatchdog(ctx, opts = {}) {
|
|
|
1647
1989
|
}
|
|
1648
1990
|
if (healthy) {
|
|
1649
1991
|
state4.consecutiveFailures = 0;
|
|
1992
|
+
state4.tier1Restarts = 0;
|
|
1650
1993
|
return;
|
|
1651
1994
|
}
|
|
1652
1995
|
state4.consecutiveFailures++;
|
|
1653
1996
|
if (state4.consecutiveFailures >= 3 && Date.now() - state4.lastRestartAt >= cooldownMs) {
|
|
1654
1997
|
const remoteExists = await verifyRemoteTunnel(ctx.cf, ctx.tunnelId);
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
} else {
|
|
1658
|
-
await tier1Restart(ctx, state4);
|
|
1659
|
-
}
|
|
1998
|
+
const run2 = !remoteExists || state4.tier1Restarts >= 3 ? tier2Reprovision : tier1Restart;
|
|
1999
|
+
await guardedRestart(ctx.fqdn, () => run2(ctx, state4));
|
|
1660
2000
|
}
|
|
1661
2001
|
};
|
|
1662
2002
|
const timer = setInterval(() => {
|
|
1663
|
-
|
|
2003
|
+
check().catch((err) => say.debug(`[watchdog] check failed: ${err.message}`));
|
|
1664
2004
|
}, intervalMs);
|
|
1665
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
1666
2005
|
activeWatchdogs.set(ctx.fqdn, { timer, ctx, state: state4 });
|
|
1667
2006
|
}
|
|
1668
2007
|
function stopTunnelWatchdog(fqdn) {
|
|
@@ -1676,26 +2015,55 @@ function stopAllWatchdogs() {
|
|
|
1676
2015
|
for (const [fqdn] of activeWatchdogs) stopTunnelWatchdog(fqdn);
|
|
1677
2016
|
}
|
|
1678
2017
|
|
|
2018
|
+
// src/core/installed-version.ts
|
|
2019
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync2 } from "fs";
|
|
2020
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
2021
|
+
function readInstalledVersion() {
|
|
2022
|
+
try {
|
|
2023
|
+
const script = realpathSync2(process.argv[1] ?? "");
|
|
2024
|
+
const pkg2 = JSON.parse(readFileSync8(join8(dirname3(script), "..", "package.json"), "utf8"));
|
|
2025
|
+
return typeof pkg2.version === "string" ? pkg2.version : void 0;
|
|
2026
|
+
} catch {
|
|
2027
|
+
return void 0;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1679
2031
|
// src/core/up-runner.ts
|
|
1680
2032
|
var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2033
|
+
var UPGRADE_RESTART_EXIT_CODE = 75;
|
|
2034
|
+
var VERSION_CHECK_INTERVAL_MS = 6e4;
|
|
2035
|
+
var LOCK_WAIT_MS = 3e4;
|
|
2036
|
+
var MAX_CREATE_BACKOFF_MS = 6e4;
|
|
1681
2037
|
function logFileFor(fqdn) {
|
|
1682
|
-
return
|
|
2038
|
+
return join9(logDir, `${serviceSlug(fqdn)}.log`);
|
|
2039
|
+
}
|
|
2040
|
+
function knownFqdn(item) {
|
|
2041
|
+
if (item.hostname) return item.hostname;
|
|
2042
|
+
return item.name !== void 0 && item.zone ? fqdnFor(item.name, item.zone) : void 0;
|
|
1683
2043
|
}
|
|
1684
2044
|
async function createWithBootRetry(cf, item, keepAlive) {
|
|
1685
|
-
|
|
2045
|
+
if (!keepAlive) return createTunnelSubdomain(cf, item);
|
|
1686
2046
|
for (let attempt = 0; ; attempt++) {
|
|
1687
2047
|
try {
|
|
1688
|
-
return await createTunnelSubdomain(cf, item);
|
|
2048
|
+
return await createTunnelSubdomain(cf, { ...item, reuse: true });
|
|
1689
2049
|
} catch (err) {
|
|
1690
|
-
|
|
1691
|
-
say.
|
|
1692
|
-
await sleep4(
|
|
2050
|
+
const wait = Math.min(2 ** attempt * 1e3, MAX_CREATE_BACKOFF_MS);
|
|
2051
|
+
say.warn(`Could not create ${item.name ?? "tunnel"} (${err.message}) \u2014 retrying in ${wait / 1e3}s`);
|
|
2052
|
+
await sleep4(wait);
|
|
1693
2053
|
}
|
|
1694
2054
|
}
|
|
1695
2055
|
}
|
|
2056
|
+
async function freshMetricsPort2() {
|
|
2057
|
+
try {
|
|
2058
|
+
return await pickFreePort();
|
|
2059
|
+
} catch {
|
|
2060
|
+
return void 0;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
1696
2063
|
async function startTunnels(cf, bin, items, opts = {}) {
|
|
1697
2064
|
const started = [];
|
|
1698
2065
|
let tornDown = false;
|
|
2066
|
+
const supervised = !!opts.keepAlive && !opts.detach;
|
|
1699
2067
|
const teardownAll = async (code) => {
|
|
1700
2068
|
if (tornDown) return;
|
|
1701
2069
|
tornDown = true;
|
|
@@ -1703,7 +2071,8 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1703
2071
|
try {
|
|
1704
2072
|
for (const s of started) {
|
|
1705
2073
|
if (opts.keepAlive) {
|
|
1706
|
-
|
|
2074
|
+
const current = getEntry(s.fqdn);
|
|
2075
|
+
await stopConnector(current?.pid ? current : { pid: s.pid, bootId: currentBootId() });
|
|
1707
2076
|
} else {
|
|
1708
2077
|
try {
|
|
1709
2078
|
await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
|
|
@@ -1718,6 +2087,12 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1718
2087
|
process.exit(code);
|
|
1719
2088
|
}
|
|
1720
2089
|
};
|
|
2090
|
+
for (const item of items) {
|
|
2091
|
+
const fqdn = knownFqdn(item);
|
|
2092
|
+
if (!fqdn) continue;
|
|
2093
|
+
if (opts.detach) await assertHostnameFree(fqdn);
|
|
2094
|
+
else await acquireHostnameLock(fqdn, { waitMs: opts.keepAlive ? LOCK_WAIT_MS : 0 });
|
|
2095
|
+
}
|
|
1721
2096
|
const spin = clack2.spinner();
|
|
1722
2097
|
spin.start(items.length > 1 ? "Creating tunnels\u2026" : "Creating tunnel\u2026");
|
|
1723
2098
|
for (const item of items) {
|
|
@@ -1725,28 +2100,35 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1725
2100
|
const result = await createWithBootRetry(cf, item, opts.keepAlive);
|
|
1726
2101
|
const fqdn = result.host.hostname;
|
|
1727
2102
|
const logFile = logFileFor(fqdn);
|
|
2103
|
+
const knownPort = await freshMetricsPort2();
|
|
1728
2104
|
const conn = startConnector({
|
|
1729
2105
|
bin,
|
|
1730
2106
|
token: result.token,
|
|
1731
2107
|
detach: !!opts.detach,
|
|
1732
2108
|
logFile,
|
|
1733
2109
|
protocol: opts.protocol,
|
|
2110
|
+
metricsPort: knownPort,
|
|
1734
2111
|
onExit: opts.detach ? void 0 : (code) => {
|
|
1735
|
-
if (
|
|
1736
|
-
|
|
1737
|
-
void
|
|
2112
|
+
if (tornDown) return;
|
|
2113
|
+
if (opts.keepAlive) {
|
|
2114
|
+
void notifyConnectorExited(fqdn);
|
|
2115
|
+
return;
|
|
1738
2116
|
}
|
|
2117
|
+
say.warn(`Connector for ${fqdn} exited.`);
|
|
2118
|
+
void teardownAll(code ?? 1);
|
|
1739
2119
|
}
|
|
1740
2120
|
});
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2121
|
+
let metricsPort = knownPort;
|
|
2122
|
+
if (!metricsPort) {
|
|
2123
|
+
await sleep4(600);
|
|
2124
|
+
if (existsSync8(logFile)) {
|
|
2125
|
+
try {
|
|
2126
|
+
metricsPort = extractMetricsPort(readFileSync9(logFile, "utf8"));
|
|
2127
|
+
} catch {
|
|
2128
|
+
}
|
|
1747
2129
|
}
|
|
1748
2130
|
}
|
|
1749
|
-
await
|
|
2131
|
+
await markConnectorRunning(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile, metricsPort, protocol: opts.protocol });
|
|
1750
2132
|
started.push({
|
|
1751
2133
|
fqdn,
|
|
1752
2134
|
subdomain: result.host.subdomain,
|
|
@@ -1754,7 +2136,7 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1754
2136
|
target: serviceUrl(item.proto, item.host ?? "localhost", item.port),
|
|
1755
2137
|
pid: conn.pid
|
|
1756
2138
|
});
|
|
1757
|
-
if (
|
|
2139
|
+
if (supervised) {
|
|
1758
2140
|
startTunnelWatchdog({
|
|
1759
2141
|
cf,
|
|
1760
2142
|
bin,
|
|
@@ -1779,6 +2161,7 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1779
2161
|
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
1780
2162
|
process.on(sig, () => void teardownAll(0));
|
|
1781
2163
|
}
|
|
2164
|
+
if (supervised) watchForUpgrade(() => void teardownAll(UPGRADE_RESTART_EXIT_CODE));
|
|
1782
2165
|
spin.message("Connecting to the Cloudflare edge\u2026");
|
|
1783
2166
|
const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
|
|
1784
2167
|
const live = healths.filter((h) => h === "healthy").length;
|
|
@@ -1787,6 +2170,20 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1787
2170
|
clack2.note(lines.join("\n"), `${live}/${started.length} live`);
|
|
1788
2171
|
say.dim(opts.keepAlive ? "Service running with auto-heal watchdog." : "Ctrl-C stops and releases them.");
|
|
1789
2172
|
}
|
|
2173
|
+
function watchForUpgrade(restart) {
|
|
2174
|
+
if (process.platform === "win32") return;
|
|
2175
|
+
const startedWith = readInstalledVersion();
|
|
2176
|
+
if (!startedWith) return;
|
|
2177
|
+
const timer = setInterval(() => {
|
|
2178
|
+
const now = readInstalledVersion();
|
|
2179
|
+
if (now && now !== startedWith) {
|
|
2180
|
+
clearInterval(timer);
|
|
2181
|
+
say.warn(`cloudtunnel ${startedWith} \u2192 ${now} installed \u2014 restarting onto the new version\u2026`);
|
|
2182
|
+
restart();
|
|
2183
|
+
}
|
|
2184
|
+
}, VERSION_CHECK_INTERVAL_MS);
|
|
2185
|
+
timer.unref();
|
|
2186
|
+
}
|
|
1790
2187
|
|
|
1791
2188
|
// src/core/resolve-domain.ts
|
|
1792
2189
|
async function resolveDomain(cf, opts, creds) {
|
|
@@ -1799,12 +2196,6 @@ async function resolveDomain(cf, opts, creds) {
|
|
|
1799
2196
|
throw new CliError("Multiple domains in this account \u2014 pick one.", { hint: "pass -d <domain>" });
|
|
1800
2197
|
}
|
|
1801
2198
|
|
|
1802
|
-
// src/core/transport-protocol.ts
|
|
1803
|
-
function parseTransportProtocol(value) {
|
|
1804
|
-
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
1805
|
-
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
2199
|
// src/commands/up.ts
|
|
1809
2200
|
function promptOrExit(value) {
|
|
1810
2201
|
if (clack3.isCancel(value)) {
|
|
@@ -1940,6 +2331,8 @@ function registerLs(program) {
|
|
|
1940
2331
|
["#", "URL", "TARGET", "PROTOCOL", "STATE", "SERVICE", "PID"],
|
|
1941
2332
|
rows.map((r) => [r.num, r.url, r.target, r.protocol, r.state, r.service, r.pid])
|
|
1942
2333
|
);
|
|
2334
|
+
const needRepair = rows.filter((r) => r.serviceIssue).length;
|
|
2335
|
+
if (needRepair) say.warn(`${needRepair} boot service(s) need repair \u2192 cloudtunnel service repair`);
|
|
1943
2336
|
});
|
|
1944
2337
|
}
|
|
1945
2338
|
|
|
@@ -1998,19 +2391,19 @@ function registerDelete(program) {
|
|
|
1998
2391
|
}
|
|
1999
2392
|
|
|
2000
2393
|
// src/commands/logs.ts
|
|
2001
|
-
import { closeSync, existsSync as
|
|
2394
|
+
import { closeSync, existsSync as existsSync9, openSync as openSync2, readFileSync as readFileSync10, readSync, statSync as statSync2, watch } from "fs";
|
|
2002
2395
|
function printTail(file, n) {
|
|
2003
|
-
const lines =
|
|
2396
|
+
const lines = readFileSync10(file, "utf8").split("\n");
|
|
2004
2397
|
const tail = lines.slice(-n).join("\n");
|
|
2005
2398
|
process.stdout.write(tail.endsWith("\n") ? tail : `${tail}
|
|
2006
2399
|
`);
|
|
2007
|
-
return
|
|
2400
|
+
return statSync2(file).size;
|
|
2008
2401
|
}
|
|
2009
2402
|
function follow(file, fromPos) {
|
|
2010
2403
|
let pos = fromPos;
|
|
2011
2404
|
say.dim("\u2014 following (Ctrl-C to stop) \u2014");
|
|
2012
2405
|
const watcher = watch(file, () => {
|
|
2013
|
-
const size =
|
|
2406
|
+
const size = statSync2(file).size;
|
|
2014
2407
|
if (size < pos) {
|
|
2015
2408
|
pos = 0;
|
|
2016
2409
|
return;
|
|
@@ -2032,7 +2425,7 @@ function follow(file, fromPos) {
|
|
|
2032
2425
|
function registerLogs(program) {
|
|
2033
2426
|
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) => {
|
|
2034
2427
|
const { fqdn, entry } = resolveTarget(name);
|
|
2035
|
-
if (!entry?.logFile || !
|
|
2428
|
+
if (!entry?.logFile || !existsSync9(entry.logFile)) {
|
|
2036
2429
|
throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
|
|
2037
2430
|
}
|
|
2038
2431
|
const n = Math.max(1, Number(opts.lines) || 50);
|
|
@@ -2044,7 +2437,7 @@ function registerLogs(program) {
|
|
|
2044
2437
|
// src/commands/relay.ts
|
|
2045
2438
|
import { openSync as openSync3 } from "fs";
|
|
2046
2439
|
import { spawn as spawn3 } from "child_process";
|
|
2047
|
-
import { join as
|
|
2440
|
+
import { join as join10 } from "path";
|
|
2048
2441
|
import pc from "picocolors";
|
|
2049
2442
|
|
|
2050
2443
|
// src/core/api-proxy-server.ts
|
|
@@ -2151,7 +2544,7 @@ function spawnDetachedRelay(sub, domain) {
|
|
|
2151
2544
|
const script = process.argv[1];
|
|
2152
2545
|
if (!script) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
2153
2546
|
ensureDirs();
|
|
2154
|
-
const logFile =
|
|
2547
|
+
const logFile = join10(logDir, `relay-${sub === "@" ? "root" : sub}.log`);
|
|
2155
2548
|
const fd = openSync3(logFile, "a", 384);
|
|
2156
2549
|
const args = [script, "relay", sub, "-d", domain, "-f", "-y"];
|
|
2157
2550
|
const child = spawn3(process.execPath, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
@@ -2209,11 +2602,68 @@ async function runRelay(sub, opts) {
|
|
|
2209
2602
|
force: opts.force,
|
|
2210
2603
|
yes: opts.yes
|
|
2211
2604
|
};
|
|
2212
|
-
await startTunnels(cf, bin, [item], {});
|
|
2605
|
+
await startTunnels(cf, bin, [item], { keepAlive: opts.keepAlive });
|
|
2213
2606
|
printRelayReady(fqdn, secret, "foreground");
|
|
2214
2607
|
}
|
|
2215
2608
|
function registerRelay(program) {
|
|
2216
|
-
program.command("relay [subdomain]").description("Expose a locked Cloudflare-API reverse proxy via a tunnel (for clients that can't reach api.cloudflare.com directly)").option("-d, --domain <domain>", "domain for the relay subdomain (prompted from a list if unset)").option("--detach", "run the relay in the background").option("--service", "register the relay as a boot service (systemd \xB7 launchd \xB7 Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt before replacing an existing record").action((subdomain, opts) => runRelay(subdomain ?? DEFAULT_SUB, opts));
|
|
2609
|
+
program.command("relay [subdomain]").description("Expose a locked Cloudflare-API reverse proxy via a tunnel (for clients that can't reach api.cloudflare.com directly)").option("-d, --domain <domain>", "domain for the relay subdomain (prompted from a list if unset)").option("--detach", "run the relay in the background").option("--service", "register the relay as a boot service (systemd \xB7 launchd \xB7 Task Scheduler)").option("--keep-alive", "supervised mode with auto-heal watchdog (used by the boot service)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt before replacing an existing record").action((subdomain, opts) => runRelay(subdomain ?? DEFAULT_SUB, opts));
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// src/commands/service.ts
|
|
2613
|
+
function describeAction(a) {
|
|
2614
|
+
const parts = [];
|
|
2615
|
+
if (a.reinstall) parts.push(`reinstall + restart (${a.reinstall.reason})`);
|
|
2616
|
+
if (a.remove.length) parts.push(`remove duplicate ${a.remove.join(", ")}`);
|
|
2617
|
+
return `${a.fqdn}: ${parts.join(" \xB7 ")}`;
|
|
2618
|
+
}
|
|
2619
|
+
async function repairWithPrompt(actions, opts = {}) {
|
|
2620
|
+
for (const a of actions) say.dim(` ${describeAction(a)}`);
|
|
2621
|
+
if (opts.dryRun) return false;
|
|
2622
|
+
const needsSudo = process.platform === "linux" ? " (uses sudo)" : "";
|
|
2623
|
+
if (!opts.yes && !await confirm(`Repair ${actions.length} boot service(s) now?${needsSudo}`)) return false;
|
|
2624
|
+
const res = runRepair(actions);
|
|
2625
|
+
for (const fqdn of res.fixed) say.ok(`Repaired ${fqdn}`);
|
|
2626
|
+
for (const f of res.failed) say.warn(`Could not repair ${f.fqdn}: ${f.error}`);
|
|
2627
|
+
if (res.failed.length) {
|
|
2628
|
+
throw new CliError(`${res.failed.length} boot service(s) could not be repaired.`, { hint: "re-run `cloudtunnel service repair`" });
|
|
2629
|
+
}
|
|
2630
|
+
return res.fixed.length > 0;
|
|
2631
|
+
}
|
|
2632
|
+
function registerService(program) {
|
|
2633
|
+
const service = program.command("service").description("Manage boot services (installed with `up --service`)");
|
|
2634
|
+
service.command("repair").description("Regenerate + restart boot services that are outdated, broken, stopped, or duplicated").option("-y, --yes", "don't ask for confirmation").option("--dry-run", "show what would be repaired without doing it").action(async (opts) => {
|
|
2635
|
+
assertServiceSupported();
|
|
2636
|
+
const services = inventoryServices();
|
|
2637
|
+
const actions = planRepair(services, { restartRunning: process.platform === "win32" });
|
|
2638
|
+
if (actions.length === 0) {
|
|
2639
|
+
say.ok(`All ${services.length} boot service(s) are up to date and running.`);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
say.info(`${actions.length} boot service(s) need repair:`);
|
|
2643
|
+
await repairWithPrompt(actions, opts);
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// src/core/service-upgrade-check.ts
|
|
2648
|
+
var INFO_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-v", "--version", "help"]);
|
|
2649
|
+
function remember(version) {
|
|
2650
|
+
saveConfig({ ...loadConfig(), serviceCheckVersion: version });
|
|
2651
|
+
}
|
|
2652
|
+
async function maybeRepairServicesAfterUpgrade(version, argv) {
|
|
2653
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
2654
|
+
const rest = argv.slice(2);
|
|
2655
|
+
if (rest[0] === "service" || rest[0] === "delete" || rest.some((a) => INFO_ARGS.has(a))) return;
|
|
2656
|
+
if (loadConfig().serviceCheckVersion === version) return;
|
|
2657
|
+
try {
|
|
2658
|
+
const actions = planRepair(inventoryServices(), { restartRunning: process.platform === "win32" });
|
|
2659
|
+
if (actions.length) {
|
|
2660
|
+
say.warn(`cloudtunnel ${version}: ${actions.length} boot service(s) need repair to stay up:`);
|
|
2661
|
+
await repairWithPrompt(actions);
|
|
2662
|
+
}
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
say.warn(`Boot-service check failed: ${err.message} \u2014 run \`cloudtunnel service repair\``);
|
|
2665
|
+
}
|
|
2666
|
+
remember(version);
|
|
2217
2667
|
}
|
|
2218
2668
|
|
|
2219
2669
|
// src/index.ts
|
|
@@ -2233,7 +2683,7 @@ function buildProgram() {
|
|
|
2233
2683
|
""
|
|
2234
2684
|
].join("\n")
|
|
2235
2685
|
);
|
|
2236
|
-
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs, registerRelay]) {
|
|
2686
|
+
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs, registerRelay, registerService]) {
|
|
2237
2687
|
register(program);
|
|
2238
2688
|
}
|
|
2239
2689
|
return program;
|
|
@@ -2247,6 +2697,7 @@ function shouldMigrate(argv) {
|
|
|
2247
2697
|
async function main() {
|
|
2248
2698
|
configureProxy();
|
|
2249
2699
|
if (shouldMigrate(process.argv)) await migrateLegacyProfiles();
|
|
2700
|
+
await maybeRepairServicesAfterUpgrade(pkg.version, process.argv);
|
|
2250
2701
|
const program = buildProgram();
|
|
2251
2702
|
try {
|
|
2252
2703
|
await program.parseAsync(process.argv);
|