windscout 0.4.3 → 0.4.5
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 +7 -1
- package/package.json +1 -1
- package/windscout.js +46 -13
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ Run `windscout` with no arguments to open the interactive picker:
|
|
|
51
51
|
| `a` / `n` | select all / unselect all (only the matching countries while a search is active) |
|
|
52
52
|
| `1`–`6` | toggle a protocol (`1`–`5` on Linux, which has no IKEv2) |
|
|
53
53
|
| `s` | toggle the speed test (off = connect-only mode) |
|
|
54
|
+
| `f` | toggle "block internet while scanning" (firewall) |
|
|
54
55
|
| `enter` | start scanning |
|
|
55
56
|
| `q` | quit (during a scan: stop and show partial results; press again to force quit) |
|
|
56
57
|
|
|
@@ -77,6 +78,7 @@ windscout -t 40 -a # longer connect timeout, also list
|
|
|
77
78
|
| `-t, --timeout` | seconds to wait for each connection | 20 |
|
|
78
79
|
| `-a, --all` | include failed attempts in the final table | off |
|
|
79
80
|
| `-n, --no-speed` | connect-only mode: skip the download test, rank by latency | picker: saved, or connect-only on first run; with `-c`: speed test on |
|
|
81
|
+
| `-f, --firewall` | block internet while scanning, except through the VPN | picker: saved, or off; with `-c`: off |
|
|
80
82
|
| `-v, --version` | print the installed version, and the update command if a newer one is on npm | |
|
|
81
83
|
|
|
82
84
|
## Connect-only mode
|
|
@@ -98,7 +100,11 @@ Both modes show an **availability grid**: one row per country, and ✓ / ✗ for
|
|
|
98
100
|
╰─────────────────────────┴────────────────────────┴─────────┴───────────┴─────╯
|
|
99
101
|
```
|
|
100
102
|
|
|
101
|
-
While scanning, press `tab` to switch between the availability grid and the speed or latency ranking. In connect-only mode the scan starts on the availability grid.
|
|
103
|
+
While scanning, press `tab` to switch between the availability grid and the speed or latency ranking. Press `p` (or `space`) to pause: the current attempt finishes, the VPN disconnects, and the scan waits until you press `p` again. Paused time doesn't count toward elapsed or remaining. In connect-only mode the scan starts on the availability grid.
|
|
104
|
+
|
|
105
|
+
## Block internet while scanning (firewall)
|
|
106
|
+
|
|
107
|
+
Between attempts the VPN is disconnected for a moment, and apps on your computer (browser, messengers, updates) can go online over your real connection. Turn on **block internet while scanning** with `f` in the picker or `--firewall`, and WindScout switches on the Windscribe app's firewall for the scan: nothing gets online except through the VPN. When the scan ends or you stop it, the firewall goes back off (if you already had it on, WindScout leaves it on). While a scan is paused, the firewall keeps blocking.
|
|
102
108
|
|
|
103
109
|
## Output
|
|
104
110
|
|
package/package.json
CHANGED
package/windscout.js
CHANGED
|
@@ -47,6 +47,8 @@ const noFlags = process.platform === "win32" || process.env.TERM_PROGRAM === "Ap
|
|
|
47
47
|
const flag = (code) => !noFlags && /^[A-Z]{2}$/.test(code) ? String.fromCodePoint(...[...code].map((c) => 0x1f1a5 + c.charCodeAt(0))) : " ";
|
|
48
48
|
let stop = 0;
|
|
49
49
|
let speedTest = true; // false = connect-only mode: skip the download test, rank by latency
|
|
50
|
+
let firewall = false; // turn on the app's firewall while scanning, so nothing goes online outside the VPN
|
|
51
|
+
let restoreFirewall = async () => {}; // set by scan(); also called on a crash so the internet isn't left blocked
|
|
50
52
|
|
|
51
53
|
// ---------- windscribe + measurements ----------
|
|
52
54
|
|
|
@@ -314,6 +316,7 @@ function loadConfig() {
|
|
|
314
316
|
countries: (c.countries ?? []).filter((x) => COUNTRIES.includes(x)),
|
|
315
317
|
protocols: (c.protocols ?? []).filter((x) => PROTOCOLS.includes(x)),
|
|
316
318
|
speed: c.speed !== false,
|
|
319
|
+
firewall: c.firewall === true,
|
|
317
320
|
};
|
|
318
321
|
} catch {
|
|
319
322
|
return null; // first run or unreadable file
|
|
@@ -415,7 +418,7 @@ function pick(saved, timeout) {
|
|
|
415
418
|
const render = () => {
|
|
416
419
|
const rows = out.rows || 24;
|
|
417
420
|
const list = visible();
|
|
418
|
-
const height = Math.max(3, rows -
|
|
421
|
+
const height = Math.max(3, rows - 20 - (update ? 1 : 0));
|
|
419
422
|
cursor = Math.max(0, Math.min(cursor, list.length - 1));
|
|
420
423
|
if (cursor < top) top = cursor;
|
|
421
424
|
if (cursor >= top + height) top = cursor - height + 1;
|
|
@@ -426,6 +429,10 @@ function pick(saved, timeout) {
|
|
|
426
429
|
const speedLine = `${dim(under("s"))} ${speedTest ? green("● speed test") : dim("○ speed test")} ${dim(speedTest
|
|
427
430
|
? "ranks by download speed · adds ~15s per working connection"
|
|
428
431
|
: "off: connect-only, ranks by latency")}`;
|
|
432
|
+
const fwLabel = "block internet while scanning (firewall)";
|
|
433
|
+
const fwLine = `${dim(under("f"))} ${firewall ? green("● " + fwLabel) : dim("○ " + fwLabel)} ${dim(firewall
|
|
434
|
+
? "only VPN traffic gets through"
|
|
435
|
+
: "off: apps may leak in between")}`;
|
|
429
436
|
const search = searching
|
|
430
437
|
? `${cyan("🔍")} ${bold(query)}${cyan("▏")}`
|
|
431
438
|
: query ? `🔍 ${query}` : dim("🔍 press / to search");
|
|
@@ -442,9 +449,9 @@ function pick(saved, timeout) {
|
|
|
442
449
|
const buttonLine = buttons.map(([id, label]) => (id === "start" ? bold(cyan(`[ ${label} ]`)) : dim(`[ ${label} ]`))).join(" ");
|
|
443
450
|
|
|
444
451
|
const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body,
|
|
445
|
-
{ caption: search, captionRight: dim(range), minWidth: Math.max(vw(protoLine), vw(speedLine)) + 4 });
|
|
452
|
+
{ caption: search, captionRight: dim(range), minWidth: Math.max(vw(protoLine), vw(speedLine), vw(fwLine)) + 4 });
|
|
446
453
|
const width = vw(table[0]);
|
|
447
|
-
const head = box([protoLine, speedLine], {
|
|
454
|
+
const head = box([protoLine, speedLine, fwLine], {
|
|
448
455
|
title: bold(cyan("WindScout")),
|
|
449
456
|
right: `${bold(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
|
|
450
457
|
width: width - 4,
|
|
@@ -460,7 +467,7 @@ function pick(saved, timeout) {
|
|
|
460
467
|
centerIn(dim(searching
|
|
461
468
|
? "type to filter · ↑↓ move · enter/esc done"
|
|
462
469
|
: `↑↓ move · space select · a/n ${query ? "all/none shown" : "all/none"} · / search`), width),
|
|
463
|
-
centerIn(dim(searching ? "" : `1-${PROTOCOLS.length} protocols · s speed test · enter start · ${query ? "esc clear · " : ""}q quit`), width),
|
|
470
|
+
centerIn(dim(searching ? "" : `1-${PROTOCOLS.length} protocols · s speed test · f firewall · enter start · ${query ? "esc clear · " : ""}q quit`), width),
|
|
464
471
|
];
|
|
465
472
|
lines.splice(head.length + table.length + 1, 0, centerIn(buttonLine, width), "");
|
|
466
473
|
if (message) lines.push(centerIn(yellow(message), width));
|
|
@@ -485,7 +492,7 @@ function pick(saved, timeout) {
|
|
|
485
492
|
return range;
|
|
486
493
|
});
|
|
487
494
|
hit = {
|
|
488
|
-
protoRow: y0 + 1, speedRow: y0 + 2, protoHits,
|
|
495
|
+
protoRow: y0 + 1, speedRow: y0 + 2, firewallRow: y0 + 3, protoHits,
|
|
489
496
|
searchRow: y0 + head.length + 1,
|
|
490
497
|
firstRow: y0 + head.length + 5, shown: shown.length,
|
|
491
498
|
buttonRow: y0 + head.length + table.length + 1, buttonHits,
|
|
@@ -510,6 +517,7 @@ function pick(saved, timeout) {
|
|
|
510
517
|
countries: COUNTRIES.filter((c) => selected.has(c)),
|
|
511
518
|
protocols: PROTOCOLS.filter((p) => protos.has(p)),
|
|
512
519
|
speed: speedTest,
|
|
520
|
+
firewall,
|
|
513
521
|
};
|
|
514
522
|
saveConfig(choice);
|
|
515
523
|
finish(choice);
|
|
@@ -535,6 +543,7 @@ function pick(saved, timeout) {
|
|
|
535
543
|
const i = hit.protoHits.findIndex(([a, b]) => x >= a && x < b);
|
|
536
544
|
if (i >= 0) protos.has(PROTOCOLS[i]) ? protos.delete(PROTOCOLS[i]) : protos.add(PROTOCOLS[i]);
|
|
537
545
|
} else if (y === hit.speedRow) speedTest = !speedTest;
|
|
546
|
+
else if (y === hit.firewallRow) firewall = !firewall;
|
|
538
547
|
else if (y === hit.buttonRow) {
|
|
539
548
|
const id = hit.buttonHits.find(([, a, b]) => x >= a && x < b)?.[0];
|
|
540
549
|
if (id === "start" && start()) return;
|
|
@@ -578,6 +587,7 @@ function pick(saved, timeout) {
|
|
|
578
587
|
else if (k === "a") list.forEach((c) => selected.add(c));
|
|
579
588
|
else if (k === "n") list.forEach((c) => selected.delete(c));
|
|
580
589
|
else if (k === "s") speedTest = !speedTest;
|
|
590
|
+
else if (k === "f") firewall = !firewall;
|
|
581
591
|
else if (/^[1-9]$/.test(str ?? "") && PROTOCOLS[Number(str) - 1]) {
|
|
582
592
|
const p = PROTOCOLS[Number(str) - 1];
|
|
583
593
|
protos.has(p) ? protos.delete(p) : protos.add(p);
|
|
@@ -599,12 +609,18 @@ async function scan(countries, protocols, timeout, tty) {
|
|
|
599
609
|
const started = now();
|
|
600
610
|
let done = 0, attemptStart = now(), current = "", step = "", frame = 0, finishedCountries = [];
|
|
601
611
|
let view = speedTest ? "ranking" : "availability"; // tab switches
|
|
612
|
+
// pause lands between attempts, so the VPN is disconnected while waiting; idle = seconds spent paused
|
|
613
|
+
let paused = false, waiting = false, waitStart = 0, idle = 0, resume = () => {};
|
|
614
|
+
// firewall: block everything outside the VPN while switching locations; put it back the way the user had it
|
|
615
|
+
const fwChanged = firewall && /^Firewall state: Off/m.test(await cli("status"));
|
|
616
|
+
if (fwChanged) await cli("firewall", "on");
|
|
617
|
+
restoreFirewall = async () => { if (fwChanged) await cli("firewall", "off"); };
|
|
602
618
|
|
|
603
619
|
const render = () => {
|
|
604
620
|
const rows = out.rows || 24;
|
|
605
|
-
const elapsed = now() - started;
|
|
621
|
+
const elapsed = now() - started - idle - (waiting ? now() - waitStart : 0);
|
|
606
622
|
// smooth bar: count the running attempt as partially done
|
|
607
|
-
const partial = Math.min(0.95, (now() - attemptStart) / attemptSeconds(timeout));
|
|
623
|
+
const partial = waiting ? 0 : Math.min(0.95, (now() - attemptStart) / attemptSeconds(timeout));
|
|
608
624
|
const fraction = (done + (done < total ? partial : 0)) / total;
|
|
609
625
|
const eta = (total - done) * (done ? elapsed / done : attemptSeconds(timeout));
|
|
610
626
|
const spinner = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"[frame++ % 10];
|
|
@@ -640,8 +656,9 @@ async function scan(countries, protocols, timeout, tty) {
|
|
|
640
656
|
"",
|
|
641
657
|
stop
|
|
642
658
|
? yellow(" stopping after the current attempt…")
|
|
659
|
+
: waiting ? yellow(` paused · VPN disconnected${fwChanged ? " · firewall blocking internet" : ""} · press p to resume`)
|
|
643
660
|
: ` ${cyan(spinner)} ${bold(current)}`,
|
|
644
|
-
stop ? "" : ` ${dim(step)} ${dim("·")} ${duration(now() - attemptStart)}`,
|
|
661
|
+
stop || waiting ? "" : ` ${dim(step)} ${dim("·")} ${duration(now() - attemptStart)}${paused && frame % 8 < 5 ? yellow(" · pausing after this attempt") : ""}`,
|
|
645
662
|
null,
|
|
646
663
|
centerIn(stats, inner),
|
|
647
664
|
], {
|
|
@@ -658,34 +675,45 @@ async function scan(countries, protocols, timeout, tty) {
|
|
|
658
675
|
while (vw(txt) > width && failed.length) txt = txt.slice(0, -8) + "…"; // ponytail: crude trim, fine for a status line
|
|
659
676
|
lines.push("", centerIn(txt, width));
|
|
660
677
|
}
|
|
661
|
-
lines.push("", centerIn(dim(`tab ${view === "ranking" ? "availability" : "ranking"} view · q stop · q twice force quit`), width), "", centerIn(dim(repoLink), width));
|
|
678
|
+
lines.push("", centerIn(dim(`tab ${view === "ranking" ? "availability" : "ranking"} view · p ${paused ? "resume" : "pause"} · q stop · q twice force quit`), width), "", centerIn(dim(repoLink), width));
|
|
662
679
|
draw(lines);
|
|
663
680
|
};
|
|
664
681
|
|
|
665
682
|
let timer;
|
|
666
683
|
if (tty) {
|
|
667
684
|
if (!fullscreen) enterScreen();
|
|
668
|
-
input.on("keypress", (str, key = {}) => {
|
|
685
|
+
input.on("keypress", async (str, key = {}) => {
|
|
669
686
|
if (key.name === "tab") {
|
|
670
687
|
view = view === "ranking" ? "availability" : "ranking";
|
|
671
688
|
render();
|
|
689
|
+
} else if (key.name === "p" || key.name === "space") {
|
|
690
|
+
paused = !paused;
|
|
691
|
+
if (!paused) resume();
|
|
692
|
+
render();
|
|
672
693
|
} else if (key.name === "q" || key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
673
694
|
if (stop++) { // second press: bail out now
|
|
674
695
|
leaveScreen();
|
|
675
696
|
console.error(`stopped. the VPN may still be connecting: run '${CLI} disconnect'`);
|
|
697
|
+
await restoreFirewall();
|
|
676
698
|
process.exit(130);
|
|
677
699
|
}
|
|
700
|
+
resume(); // a paused scan stops right away
|
|
678
701
|
render();
|
|
679
702
|
}
|
|
680
703
|
});
|
|
681
704
|
out.on("resize", render);
|
|
682
705
|
timer = setInterval(render, 120);
|
|
683
706
|
} else {
|
|
684
|
-
process.on("SIGINT", () => { if (stop++) process.exit(130); });
|
|
707
|
+
process.on("SIGINT", async () => { if (stop++) await restoreFirewall(), process.exit(130); });
|
|
685
708
|
}
|
|
686
709
|
|
|
687
710
|
outer: for (const loc of countries) {
|
|
688
711
|
for (const proto of protocols) {
|
|
712
|
+
if (paused && !stop) {
|
|
713
|
+
waiting = true, waitStart = now();
|
|
714
|
+
await new Promise((r) => (resume = r));
|
|
715
|
+
waiting = false, idle += now() - waitStart;
|
|
716
|
+
}
|
|
689
717
|
if (stop) break outer;
|
|
690
718
|
current = `${flag(loc)} ${countryName(loc)} ${dim("·")} ${proto}`;
|
|
691
719
|
attemptStart = now();
|
|
@@ -698,6 +726,7 @@ async function scan(countries, protocols, timeout, tty) {
|
|
|
698
726
|
}
|
|
699
727
|
finishedCountries.push(loc); // table/failed list refresh per finished country
|
|
700
728
|
}
|
|
729
|
+
await restoreFirewall();
|
|
701
730
|
|
|
702
731
|
if (tty) {
|
|
703
732
|
clearInterval(timer);
|
|
@@ -767,6 +796,7 @@ Options:
|
|
|
767
796
|
-t, --timeout SEC seconds to wait for each connection (default: 20)
|
|
768
797
|
-a, --all also show failed attempts in the final table
|
|
769
798
|
-n, --no-speed connect-only: skip the download test, rank by latency (faster scan)
|
|
799
|
+
-f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
|
|
770
800
|
-v, --version show the installed version (and whether a newer one is out)
|
|
771
801
|
-h, --help show this help
|
|
772
802
|
|
|
@@ -783,6 +813,7 @@ async function main() {
|
|
|
783
813
|
help: { type: "boolean", short: "h", default: false },
|
|
784
814
|
version: { type: "boolean", short: "v", default: false },
|
|
785
815
|
"no-speed": { type: "boolean", short: "n", default: false },
|
|
816
|
+
firewall: { type: "boolean", short: "f", default: false },
|
|
786
817
|
},
|
|
787
818
|
});
|
|
788
819
|
if (a.help) return console.log(HELP);
|
|
@@ -810,13 +841,14 @@ async function main() {
|
|
|
810
841
|
const saved = loadConfig();
|
|
811
842
|
if (protocols && saved) saved.protocols = protocols.filter((p) => PROTOCOLS.includes(p));
|
|
812
843
|
speedTest = !a["no-speed"] && (saved?.speed ?? false); // first run: connect-only
|
|
844
|
+
firewall = a.firewall || (saved?.firewall ?? false);
|
|
813
845
|
const choice = await pick(saved, timeout);
|
|
814
846
|
if (!choice) return;
|
|
815
847
|
countries = choice.countries;
|
|
816
848
|
protocols = protocols ?? choice.protocols;
|
|
817
849
|
}
|
|
818
850
|
protocols ??= PROTOCOLS;
|
|
819
|
-
if (a.countries) speedTest = !a["no-speed"];
|
|
851
|
+
if (a.countries) speedTest = !a["no-speed"], firewall = a.firewall;
|
|
820
852
|
|
|
821
853
|
const results = await scan(countries, protocols, timeout, tty);
|
|
822
854
|
if (stop) console.log(yellow("interrupted, showing partial results"));
|
|
@@ -825,8 +857,9 @@ async function main() {
|
|
|
825
857
|
if (update) console.error("\n" + yellow(updateNotice()));
|
|
826
858
|
}
|
|
827
859
|
|
|
828
|
-
main().catch((e) => {
|
|
860
|
+
main().catch(async (e) => {
|
|
829
861
|
leaveScreen();
|
|
830
862
|
console.error(`windscout: ${e.message}`);
|
|
863
|
+
await restoreFirewall().catch(() => console.error(`the firewall may still be on: run '${CLI} firewall off'`));
|
|
831
864
|
process.exit(1);
|
|
832
865
|
});
|