@dutodev/cli 0.4.4 → 0.4.6
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 +1 -1
- package/dist/{fullscreen-5437X7MP.js → fullscreen-EPWB53EP.js} +2 -0
- package/dist/index.js +153 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ Com os túneis no ar, o terminal mostra cada túnel (URL, destino local, requisi
|
|
|
106
106
|
| `tab`, `↑↓` | Alterna entre túneis e requisições e move a seleção |
|
|
107
107
|
| `q` ou Ctrl+C | Fecha os túneis e sai |
|
|
108
108
|
|
|
109
|
-
Se o túnel está no ar mas nada responde na porta local, quem abrir o endereço no navegador vê uma página do Duto que
|
|
109
|
+
Se o túnel está no ar mas nada responde na porta local, quem abrir o endereço no navegador vê uma página do Duto que volta sozinha quando o serviço sobe. Ela espera por uma conexão só (SSE): a CLI checa a porta na sua máquina e avisa a página assim que o serviço responde, sem recarregar de tempos em tempos. Clientes que não são navegador (`curl`, APIs) recebem a mesma resposta em texto. Em ambos os casos o status é `503` com `Retry-After: 3`. Os caminhos `/__duto/…` são reservados para isso e nunca chegam ao seu app.
|
|
110
110
|
|
|
111
111
|
Fora de um terminal interativo (pipe, CI) ou com `--no-ui`, a CLI imprime um log simples. No Windows a tela cheia vem desligada; use `--ui` para tentar.
|
|
112
112
|
|
|
@@ -373,6 +373,7 @@ async function withFullscreen(stdout, run2) {
|
|
|
373
373
|
const onSignal = () => ac.abort();
|
|
374
374
|
process.on("SIGINT", onSignal);
|
|
375
375
|
process.on("SIGTERM", onSignal);
|
|
376
|
+
process.on("SIGHUP", onSignal);
|
|
376
377
|
process.once("exit", restore);
|
|
377
378
|
stdout.write(ENTER_SCREEN);
|
|
378
379
|
try {
|
|
@@ -380,6 +381,7 @@ async function withFullscreen(stdout, run2) {
|
|
|
380
381
|
} finally {
|
|
381
382
|
process.off("SIGINT", onSignal);
|
|
382
383
|
process.off("SIGTERM", onSignal);
|
|
384
|
+
process.off("SIGHUP", onSignal);
|
|
383
385
|
process.off("exit", restore);
|
|
384
386
|
restore();
|
|
385
387
|
}
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var fail = (msg) => `${paint("err", "\u2716", process.stderr)} ${msg}`;
|
|
|
33
33
|
|
|
34
34
|
// src/index.ts
|
|
35
35
|
import { hostname } from "os";
|
|
36
|
-
import { relative } from "path";
|
|
36
|
+
import { join as join5, relative } from "path";
|
|
37
37
|
import cac from "cac";
|
|
38
38
|
|
|
39
39
|
// src/api.ts
|
|
@@ -368,6 +368,24 @@ var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">":
|
|
|
368
368
|
function wantsHtml(accept) {
|
|
369
369
|
return /\btext\/html\b/i.test(accept ?? "");
|
|
370
370
|
}
|
|
371
|
+
var WAIT_SCRIPT = `
|
|
372
|
+
<script>
|
|
373
|
+
(function () {
|
|
374
|
+
var delay = 3000;
|
|
375
|
+
function poll() {
|
|
376
|
+
setTimeout(function () {
|
|
377
|
+
fetch(location.href, { method: "HEAD", cache: "no-store" })
|
|
378
|
+
.then(function (r) { if (r.status !== 503) location.reload(); else again(); })
|
|
379
|
+
.catch(again);
|
|
380
|
+
}, delay);
|
|
381
|
+
}
|
|
382
|
+
function again() { delay = Math.min(delay * 2, 30000); poll(); }
|
|
383
|
+
if (!window.EventSource) return poll();
|
|
384
|
+
var fails = 0, es = new EventSource("/__duto/espera");
|
|
385
|
+
es.addEventListener("up", function () { es.close(); location.reload(); });
|
|
386
|
+
es.onerror = function () { if (++fails >= 3) { es.close(); poll(); } };
|
|
387
|
+
})();
|
|
388
|
+
</script>`;
|
|
371
389
|
function unavailablePage(o) {
|
|
372
390
|
const tunnel2 = esc(o.tunnel);
|
|
373
391
|
const local = `localhost:${o.port}`;
|
|
@@ -376,7 +394,7 @@ function unavailablePage(o) {
|
|
|
376
394
|
<head>
|
|
377
395
|
<meta charset="utf-8">
|
|
378
396
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
379
|
-
|
|
397
|
+
<meta name="robots" content="noindex">
|
|
380
398
|
<title>Nada respondendo em ${local} \xB7 duto</title>
|
|
381
399
|
<style>
|
|
382
400
|
:root{--bg:#f6f6f3;--fg:#0f1110;--muted:#5f6360;--line:#dcddd8;--card:#fff;--signal:#2dd4bf;--signal-text:#0f766e}
|
|
@@ -400,8 +418,8 @@ h1 code{font-size:.86em;font-weight:600}
|
|
|
400
418
|
<h1>Nada respondendo em <code>${local}</code></h1>
|
|
401
419
|
<p>O t\xFAnel <code>${tunnel2}</code> est\xE1 no ar, mas o servi\xE7o local n\xE3o respondeu. Suba o servidor na m\xE1quina que abriu o t\xFAnel.</p>
|
|
402
420
|
<p>Se voc\xEA s\xF3 recebeu este link, avise quem o enviou.</p>
|
|
403
|
-
<div class="retry">${o.retry ?
|
|
404
|
-
</main
|
|
421
|
+
<div class="retry">${o.retry ? `<b>Aguardando o servi\xE7o.</b> A p\xE1gina volta sozinha quando ele responder.<noscript> Recarregue quando o servi\xE7o estiver no ar.</noscript>` : "Recarregue a p\xE1gina quando o servi\xE7o estiver no ar."}</div>
|
|
422
|
+
</main>${o.retry ? WAIT_SCRIPT : ""}
|
|
405
423
|
</body>
|
|
406
424
|
</html>
|
|
407
425
|
`;
|
|
@@ -485,16 +503,64 @@ function rewriteLocation(location, localHost2, publicHost) {
|
|
|
485
503
|
function hostOf(req) {
|
|
486
504
|
return (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
|
|
487
505
|
}
|
|
506
|
+
var DUTO_PREFIX = "/__duto/";
|
|
507
|
+
var WAIT_PATH = "/__duto/espera";
|
|
508
|
+
var WAIT_MAX_MS = 10 * 6e4;
|
|
509
|
+
async function isUp(port) {
|
|
510
|
+
return await probe("127.0.0.1", port) || await probe("::1", port);
|
|
511
|
+
}
|
|
512
|
+
function waitForService(res, port, checkMs, pingMs) {
|
|
513
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-accel-buffering": "no" });
|
|
514
|
+
res.write(": duto\n\n");
|
|
515
|
+
let busy = false;
|
|
516
|
+
const stop = () => {
|
|
517
|
+
clearInterval(check);
|
|
518
|
+
clearInterval(ping);
|
|
519
|
+
clearTimeout(limit);
|
|
520
|
+
};
|
|
521
|
+
const check = setInterval(async () => {
|
|
522
|
+
if (busy) return;
|
|
523
|
+
busy = true;
|
|
524
|
+
const up = await isUp(port);
|
|
525
|
+
busy = false;
|
|
526
|
+
if (up && !res.writableEnded) {
|
|
527
|
+
stop();
|
|
528
|
+
res.end("event: up\ndata: 1\n\n");
|
|
529
|
+
}
|
|
530
|
+
}, checkMs);
|
|
531
|
+
const ping = setInterval(() => res.write(": ping\n\n"), pingMs);
|
|
532
|
+
const limit = setTimeout(() => {
|
|
533
|
+
stop();
|
|
534
|
+
res.end();
|
|
535
|
+
}, WAIT_MAX_MS);
|
|
536
|
+
res.on("close", stop);
|
|
537
|
+
}
|
|
488
538
|
async function startProxy(o) {
|
|
489
539
|
const max = o.maxBody ?? MAX_BODY;
|
|
490
540
|
let nextId = 1;
|
|
541
|
+
const routeWaitMs = o.routeWaitMs ?? 3e3;
|
|
542
|
+
async function routeFor(req) {
|
|
543
|
+
const host = hostOf(req);
|
|
544
|
+
for (let waited = 0; ; waited += 50) {
|
|
545
|
+
const route = o.routes.get(host);
|
|
546
|
+
if (route || waited >= routeWaitMs) return route;
|
|
547
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
491
550
|
const server = createServer(async (req, res) => {
|
|
492
|
-
|
|
551
|
+
req.pause();
|
|
552
|
+
const route = await routeFor(req);
|
|
493
553
|
if (!route) {
|
|
494
554
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
495
555
|
res.end("T\xFAnel n\xE3o encontrado nesta sess\xE3o");
|
|
496
556
|
return;
|
|
497
557
|
}
|
|
558
|
+
if ((req.url ?? "").startsWith(DUTO_PREFIX)) {
|
|
559
|
+
if (req.url === WAIT_PATH && req.method === "GET") return waitForService(res, route.port, o.waitCheckMs ?? 1e3, o.waitPingMs ?? 25e3);
|
|
560
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
561
|
+
res.end("Rota reservada do Duto");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
498
564
|
const id = nextId++;
|
|
499
565
|
const started = Date.now();
|
|
500
566
|
const headers = rewrite(req.headers, route);
|
|
@@ -563,7 +629,7 @@ async function startProxy(o) {
|
|
|
563
629
|
});
|
|
564
630
|
server.on("upgrade", async (req, client, head) => {
|
|
565
631
|
client.on("error", () => client.destroy());
|
|
566
|
-
const route =
|
|
632
|
+
const route = await routeFor(req);
|
|
567
633
|
if (!route) return void client.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
568
634
|
const id = nextId++;
|
|
569
635
|
const started = Date.now();
|
|
@@ -628,6 +694,74 @@ async function startProxy(o) {
|
|
|
628
694
|
};
|
|
629
695
|
}
|
|
630
696
|
|
|
697
|
+
// src/session/orphans.ts
|
|
698
|
+
import { spawnSync } from "child_process";
|
|
699
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
700
|
+
import { join as join4 } from "path";
|
|
701
|
+
var fileOf = (dir, cfPid) => join4(dir, `cloudflared-${cfPid}.json`);
|
|
702
|
+
function recordCloudflared(dir, cfPid, sessionPid = process.pid) {
|
|
703
|
+
try {
|
|
704
|
+
mkdirSync3(dir, { recursive: true });
|
|
705
|
+
writeFileSync3(fileOf(dir, cfPid), JSON.stringify({ sessionPid, cfPid, at: Date.now() }));
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function forgetCloudflared(dir, cfPid) {
|
|
710
|
+
rmSync2(fileOf(dir, cfPid), { force: true });
|
|
711
|
+
}
|
|
712
|
+
var realProc = {
|
|
713
|
+
alive(pid) {
|
|
714
|
+
try {
|
|
715
|
+
process.kill(pid, 0);
|
|
716
|
+
return true;
|
|
717
|
+
} catch (err) {
|
|
718
|
+
return err.code === "EPERM";
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
isCloudflared(pid) {
|
|
722
|
+
try {
|
|
723
|
+
if (process.platform === "linux") return readFileSync3(`/proc/${pid}/cmdline`, "utf8").includes("cloudflared");
|
|
724
|
+
if (process.platform === "darwin") return spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).stdout.includes("cloudflared");
|
|
725
|
+
} catch {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
},
|
|
730
|
+
kill(pid) {
|
|
731
|
+
try {
|
|
732
|
+
process.kill(pid, "SIGTERM");
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
function reapOrphans(dir, proc = realProc) {
|
|
738
|
+
if (!existsSync4(dir)) return 0;
|
|
739
|
+
let killed = 0;
|
|
740
|
+
for (const name of readdirSync(dir)) {
|
|
741
|
+
if (!/^cloudflared-\d+\.json$/.test(name)) continue;
|
|
742
|
+
const path = join4(dir, name);
|
|
743
|
+
let rec;
|
|
744
|
+
try {
|
|
745
|
+
rec = JSON.parse(readFileSync3(path, "utf8"));
|
|
746
|
+
} catch {
|
|
747
|
+
rmSync2(path, { force: true });
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const { sessionPid, cfPid } = rec;
|
|
751
|
+
if (typeof sessionPid !== "number" || typeof cfPid !== "number") {
|
|
752
|
+
rmSync2(path, { force: true });
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (proc.alive(sessionPid)) continue;
|
|
756
|
+
if (proc.alive(cfPid) && proc.isCloudflared(cfPid)) {
|
|
757
|
+
proc.kill(cfPid);
|
|
758
|
+
killed++;
|
|
759
|
+
}
|
|
760
|
+
rmSync2(path, { force: true });
|
|
761
|
+
}
|
|
762
|
+
return killed;
|
|
763
|
+
}
|
|
764
|
+
|
|
631
765
|
// src/session/records.ts
|
|
632
766
|
var RecordStore = class {
|
|
633
767
|
constructor(max = 500) {
|
|
@@ -709,6 +843,7 @@ var Session = class {
|
|
|
709
843
|
}
|
|
710
844
|
/** Abre os túneis em sequência; falhas ficam no estado e não interrompem os outros. */
|
|
711
845
|
async start(specs) {
|
|
846
|
+
if (this.o.runDir) reapOrphans(this.o.runDir);
|
|
712
847
|
this.proxy = await startProxy({ routes: this.routes, store: this.records });
|
|
713
848
|
this.tunnels = specs.map((s) => this.blank(s, false));
|
|
714
849
|
this.emit();
|
|
@@ -797,6 +932,11 @@ var Session = class {
|
|
|
797
932
|
stdio: ["ignore", "ignore", "pipe"]
|
|
798
933
|
});
|
|
799
934
|
this.child = c;
|
|
935
|
+
const runDir = this.o.runDir;
|
|
936
|
+
if (runDir && c.pid) {
|
|
937
|
+
recordCloudflared(runDir, c.pid);
|
|
938
|
+
c.once("exit", () => forgetCloudflared(runDir, c.pid));
|
|
939
|
+
}
|
|
800
940
|
let buf = "";
|
|
801
941
|
c.stderr?.on("data", (chunk) => {
|
|
802
942
|
if (c !== this.child) return;
|
|
@@ -954,21 +1094,23 @@ async function runSession(specs, source, flags) {
|
|
|
954
1094
|
} catch (err) {
|
|
955
1095
|
fail2(err instanceof Error ? err.message : String(err));
|
|
956
1096
|
}
|
|
957
|
-
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source });
|
|
1097
|
+
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join5(dutoHome(), "run") });
|
|
958
1098
|
if (wantsUi(flags)) {
|
|
959
|
-
const { runFullscreen } = await import("./fullscreen-
|
|
960
|
-
process.exit(await runFullscreen(session, specs, "0.4.
|
|
1099
|
+
const { runFullscreen } = await import("./fullscreen-EPWB53EP.js");
|
|
1100
|
+
process.exit(await runFullscreen(session, specs, "0.4.6"));
|
|
961
1101
|
}
|
|
962
|
-
console.log(`${mark()} ${paint("muted", `${"0.4.
|
|
1102
|
+
console.log(`${mark()} ${paint("muted", `${"0.4.6"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
|
|
963
1103
|
const stopLog = runPlain(session, (l) => console.log(l));
|
|
964
1104
|
const stopped = new Promise((resolve) => {
|
|
965
1105
|
const onSignal = () => {
|
|
966
1106
|
process.off("SIGINT", onSignal);
|
|
967
1107
|
process.off("SIGTERM", onSignal);
|
|
1108
|
+
process.off("SIGHUP", onSignal);
|
|
968
1109
|
resolve();
|
|
969
1110
|
};
|
|
970
1111
|
process.on("SIGINT", onSignal);
|
|
971
1112
|
process.on("SIGTERM", onSignal);
|
|
1113
|
+
process.on("SIGHUP", onSignal);
|
|
972
1114
|
});
|
|
973
1115
|
await Promise.race([session.start(specs), stopped]);
|
|
974
1116
|
await stopped;
|
|
@@ -1046,7 +1188,7 @@ cli.help(
|
|
|
1046
1188
|
body: s.body.replace(/^\s*For more info, run any command with the `--help` flag:/m, " Mais detalhes de um comando: duto <comando> --help").replace("Display this message", "Mostra esta ajuda").replace("Display version number", "Mostra a vers\xE3o")
|
|
1047
1189
|
}))
|
|
1048
1190
|
);
|
|
1049
|
-
cli.version("0.4.
|
|
1191
|
+
cli.version("0.4.6");
|
|
1050
1192
|
try {
|
|
1051
1193
|
cli.parse(process.argv, { run: false });
|
|
1052
1194
|
await cli.runMatchedCommand();
|