windscout 0.7.0 → 0.7.1
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 -5
- package/dist/cli.js +30 -14
- package/dist/config.js +19 -1
- package/dist/docker.js +3 -3
- package/dist/scan.js +7 -15
- package/dist/tui/picker.js +35 -31
- package/dist/tui/progress.js +6 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ Run `windscout` with no arguments to open the interactive picker:
|
|
|
116
116
|
|
|
117
117
|
Your selection is saved to `~/.config/windscout/config.json` (`%USERPROFILE%\.config\windscout\config.json` on Windows) and preselected next time.
|
|
118
118
|
|
|
119
|
-
After you start, the Start button spins (`[ ⠋ Starting… ]`) until the scan's screen shows, which can take a moment when it turns the firewall on or starts docker. While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting, measuring latency or measuring speed). The table of working locations refreshes after each country. When the scan finishes, the final report is printed to your normal terminal. It ends with the best connection and the other working ones, each with its `windscribe-cli connect` command. At the prompt, press enter to connect to the best one, type another one's number to connect to that, or `q` to quit.
|
|
119
|
+
After you start, the Start button spins (`[ ⠋ Starting… ]`) until the scan's screen shows, which can take a moment when it turns the firewall on or starts docker. While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting, measuring latency or measuring speed). The table of working locations refreshes after each country. When the scan finishes, the final report is printed to your normal terminal. It ends with the best connection and the other working ones, each with its `windscribe-cli connect` command. At the prompt, press enter or `0` to connect to the best one, type another one's number and press enter to connect to that, or press `q` to quit. `0` and `q` work right away, without enter.
|
|
120
120
|
|
|
121
121
|
### Non-interactive
|
|
122
122
|
|
|
@@ -164,7 +164,7 @@ Press `o` in the picker, or pass `-o`, to choose what a scan tries and in which
|
|
|
164
164
|
| default | `default` | the selected countries × protocols, the countries in list order |
|
|
165
165
|
| by random country | `random` | the same, with the countries shuffled, a new order each run |
|
|
166
166
|
| by last success result first | `last-first` | the same, but the [last result](#last-result-rescan-and-connect)'s working connections among them go first, best first and on their own port, then the rest. With one country selected, only that country is scanned, its saved connections first. The line shows how many saved connections are in your selection (`1 of 7 saved selected · 3h ago`) |
|
|
167
|
-
| only the last results | `last-only` | only the last result's working connections, each on its own protocol and port. The countries, protocols and all ports are skipped, so the picker greys them out and locks them
|
|
167
|
+
| only the last results | `last-only` | only the last result's working connections, each on its own protocol and port. The countries, protocols and all ports are skipped, so the picker greys them out and locks them. Its country list shows just the countries this scan tries, all selected; your own selection comes back when you change the type, and stays for next time. With `-o last-only`, `-c` isn't needed |
|
|
168
168
|
|
|
169
169
|
The two types by the last result need a saved scan. Until there is one, choosing either turns `[ ▶ Start ]` off (greyed out), and a warning takes the estimate's place: `no saved scan yet, so start is off: press o for another scanning type`. Without the picker, `-o last-first` and `-o last-only` stop with an error instead. The estimate counts each saved connection once: `tcp` and `tcp:443` are the same attempt when 443 is tcp's default port.
|
|
170
170
|
|
|
@@ -198,7 +198,7 @@ Both modes show an **availability grid**: one row per country, and ✓ / ✗ for
|
|
|
198
198
|
╰─────────────────────────┴────────────────────────┴─────────┴───────────┴─────╯
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
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.
|
|
201
|
+
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. The scan starts on the ranking (*Working so far*) until you switch: then the next scans start on the table you left it on (saved to `~/.config/windscout/view.json`).
|
|
202
202
|
|
|
203
203
|
## All ports
|
|
204
204
|
|
|
@@ -248,7 +248,7 @@ Then the first parallel scan builds a local image, `windscout-worker`, with Node
|
|
|
248
248
|
|
|
249
249
|
The mirrors come from [Mirava](https://github.com/MiravaOrg/Mirava)'s list of mirrors in Iran; they come and go, and that's where to look for new ones.
|
|
250
250
|
|
|
251
|
-
**Kernel check:** each container first checks that docker's Linux kernel gives it what Windscribe needs: a tunnel device (tun) and a firewall (nftables). If not, the scan stops and says so, rather than showing every location as blocked. Windscribe's WireGuard also needs nftables' `fib` in its firewall table, which Docker Desktop's own Linux VM lacks (on macOS, and on Windows without WSL 2). There,
|
|
251
|
+
**Kernel check:** each container first checks that docker's Linux kernel gives it what Windscribe needs: a tunnel device (tun) and a firewall (nftables). If not, the scan stops and says so, rather than showing every location as blocked. Windscribe's WireGuard also needs nftables' `fib` in its firewall table, which Docker Desktop's own Linux VM lacks (on macOS, and on Windows without WSL 2). There, a parallel scan skips WireGuard (see below).
|
|
252
252
|
|
|
253
253
|
**Login:** the containers' Windscribe apps log in with the session that the Windscribe app on this computer is logged in with, so they never log in with your password. Windscribe locks an account that logs in many times at once, and a login per container would do that. WindScout reads the session from the app's settings (`~/.config/Windscribe/Windscribe2.conf` on Linux, the `com.windscribe.Windscribe2` defaults on macOS, `HKEY_CURRENT_USER\Software\Windscribe\Windscribe2` in the registry on Windows) and hands it to each container on its standard input, not on a command line, where other programs could see it. Each container's docker volume (`windscout-1`, `windscout-2`, …) keeps its app's settings for the next scans, with the WireGuard keys it registered with your account. A volume that another account used starts over. If you log out of the Windscribe app, the containers' session ends too, and the next scan gives them the new one.
|
|
254
254
|
|
|
@@ -260,7 +260,7 @@ WindScout 0.6.0 asked for your username and password instead, and could save the
|
|
|
260
260
|
|
|
261
261
|
**Differences from a normal scan:**
|
|
262
262
|
|
|
263
|
-
- IKEv2, and WireGuard when the kernel check finds it can't run in the containers
|
|
263
|
+
- The containers do the whole scan: nothing runs on this computer. What they can't run is skipped: IKEv2 (Windscribe's Linux app has none), and WireGuard when the kernel check finds it can't run in the containers. The picker's docker line says so (`ikev2 skipped`), its estimate leaves them out, and a line after the scan lists them. To try them, scan without docker. If every container stops, the scan ends there, with what they found, and says why.
|
|
264
264
|
- This computer's own Windscribe connection is disconnected at the start and stays off while the containers scan, since it would carry their traffic too.
|
|
265
265
|
- No firewall: "block internet while scanning" isn't used, since it would block the containers' traffic too.
|
|
266
266
|
|
package/dist/cli.js
CHANGED
|
@@ -49,10 +49,14 @@ Examples: windscout -c DE,NL,TR -p stealth,tcp:443
|
|
|
49
49
|
windscout connect`;
|
|
50
50
|
// "DE, NL,,TR" → ["DE", "NL", "TR"]
|
|
51
51
|
const list = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
52
|
-
|
|
52
|
+
/** The typed answer; one of instant is taken as soon as it's the whole answer, without enter */
|
|
53
|
+
const ask = (question, instant = []) => new Promise((resolve) => {
|
|
53
54
|
const rl = createInterface({ input: process.stdin, output: out });
|
|
55
|
+
// runs after readline's own keypress listener, so rl.line has the key in it
|
|
56
|
+
const onKey = () => instant.includes(rl.line.toLowerCase()) && (out.write("\n"), resolve(rl.line), rl.close());
|
|
57
|
+
process.stdin.on("keypress", onKey);
|
|
54
58
|
rl.on("SIGINT", () => rl.close()); // Ctrl-C / Ctrl-D: no answer, null
|
|
55
|
-
rl.on("close", () => resolve(null));
|
|
59
|
+
rl.on("close", () => (process.stdin.off("keypress", onKey), resolve(null)));
|
|
56
60
|
rl.question(question, (a) => (resolve(a.trim()), rl.close()));
|
|
57
61
|
});
|
|
58
62
|
/** Runs a result's connect command and checks it as the scan does: connected, on its port, with internet through it.
|
|
@@ -66,11 +70,12 @@ async function connectTo(r, timeout) {
|
|
|
66
70
|
await disconnect();
|
|
67
71
|
return false;
|
|
68
72
|
}
|
|
69
|
-
/** Asks which numbered connection to make (enter: the best), then runs its command and waits for it to connect
|
|
73
|
+
/** Asks which numbered connection to make (enter: the best), then runs its command and waits for it to connect.
|
|
74
|
+
* 0 and q need no enter; the other numbers do, since 1 may be the start of 10 */
|
|
70
75
|
async function offerConnect(choices, timeout) {
|
|
71
76
|
const others = choices.length > 2 ? `, 1-${choices.length - 1} = other` : choices.length > 1 ? ", 1 = other" : "";
|
|
72
77
|
for (;;) {
|
|
73
|
-
const a = await ask(`\nconnect to # (enter or 0 = best${others}, q = quit):
|
|
78
|
+
const a = await ask(`\nconnect to # (enter or 0 = best${others}, q = quit): `, ["0", "q"]);
|
|
74
79
|
if (a === null || /^q(uit)?$/i.test(a))
|
|
75
80
|
return;
|
|
76
81
|
const r = a === "" ? choices[0] : /^\d+$/.test(a) ? choices[Number(a)] : undefined;
|
|
@@ -218,34 +223,45 @@ async function main() {
|
|
|
218
223
|
const attempts = plan(order, countries, protocols, fromLast?.results, lists);
|
|
219
224
|
// the tables' columns: the protocols asked for, and the saved connections' own (only those, when only they are scanned)
|
|
220
225
|
const scanned = [...new Set([...(order === "last-only" ? [] : protocols), ...attempts.map((x) => x.protocol)])];
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const inContainers = attempts.filter((x) => !
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
// a parallel scan is the containers' alone: the protocols they can't run (IKEv2, WireGuard on some kernels) aren't tried
|
|
227
|
+
let probers = [probe], skipped = new Set();
|
|
228
|
+
const inContainers = () => attempts.filter((x) => !skipped.has(x.protocol));
|
|
229
|
+
const nothingLeft = () => new Error(`the docker containers can't run ${[...skipped].join(" or ")}, and nothing else is selected:`
|
|
230
|
+
+ " scan without --docker (d in the picker)");
|
|
231
|
+
if (docker && attempts.length) {
|
|
232
|
+
skipped = notInContainers(scanned, loadDockerWireGuard()); // as far as the last parallel scan knows
|
|
233
|
+
if (!inContainers().length)
|
|
234
|
+
throw nothingLeft();
|
|
235
|
+
const started = await startParallel(inContainers().length);
|
|
226
236
|
if (!started)
|
|
227
237
|
return leaveScreen();
|
|
228
238
|
saveDockerWireGuard(started.wireguard);
|
|
229
239
|
probers = started.probers;
|
|
230
|
-
|
|
240
|
+
skipped = notInContainers(scanned, started.wireguard); // their kernel may not run WireGuard after all, or may now
|
|
241
|
+
if (!inContainers().length) {
|
|
242
|
+
await stopWorkers();
|
|
243
|
+
throw nothingLeft();
|
|
244
|
+
}
|
|
231
245
|
await windscribe("disconnect"); // a VPN connection here would carry the containers' traffic too
|
|
232
246
|
}
|
|
233
|
-
const scan = new Scan(countries, scanned, { timeout, speedTest, only:
|
|
247
|
+
const scan = new Scan(countries, scanned.filter((p) => !skipped.has(p)), { timeout, speedTest, only: inContainers(), stopAtFirst }, probers);
|
|
234
248
|
// not with containers: their traffic goes out through this computer, and the firewall would block it
|
|
235
|
-
const blocking = firewall && !
|
|
249
|
+
const blocking = firewall && !docker && (await blockInternet());
|
|
236
250
|
const view = tty ? showProgress(scan, blocking) : logProgress(scan);
|
|
237
251
|
await scan.run(view);
|
|
238
|
-
if (
|
|
252
|
+
if (docker)
|
|
239
253
|
await stopWorkers();
|
|
240
254
|
await restoreFirewall();
|
|
241
255
|
view.close();
|
|
242
256
|
for (const e of new Set(scan.errors))
|
|
243
257
|
console.error(yellow(e));
|
|
258
|
+
if (skipped.size)
|
|
259
|
+
console.error(yellow(`not tried: ${[...skipped].join(", ")}, which the docker containers can't run (a scan without --docker can)`));
|
|
244
260
|
if (scan.found)
|
|
245
261
|
console.log(green("stopped at the first connection that held up (a speed test, then staying up)"));
|
|
246
262
|
else if (scan.stopping)
|
|
247
263
|
console.log(yellow("interrupted, showing partial results"));
|
|
248
|
-
const choices = report(scan.results,
|
|
264
|
+
const choices = report(scan.results, scan.protocols, { all: a.all, speedTest: speedTest || stopAtFirst }); // stop at the first measures speed
|
|
249
265
|
// its working connections replace the saved ones it tried, so what stopped working drops out; the saved ones it didn't try
|
|
250
266
|
// (other countries, or what a stopped scan didn't get to) stay, after its own; a scan where nothing worked keeps the last
|
|
251
267
|
const tried = new Set(scan.results.filter((r) => r.status !== "interrupted").map((r) => `${r.query} ${r.protocol}`));
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The picker's selection, what the last parallel scan found its containers can do,
|
|
1
|
+
// The picker's selection, what the last parallel scan found its containers can do, the last scan's working results, and the
|
|
2
|
+
// scan screen's table, remembered between runs.
|
|
2
3
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
@@ -11,6 +12,7 @@ export const MIN_TIMEOUT = 10; // seconds; shorter cuts off connections that wou
|
|
|
11
12
|
export const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "windscout", "config.json");
|
|
12
13
|
const DOCKER_FILE = join(dirname(CONFIG), "docker.json"); // what the last parallel scan found its containers can do
|
|
13
14
|
export const LAST_FILE = join(dirname(CONFIG), "last.json"); // the last scan's working connections
|
|
15
|
+
const VIEW_FILE = join(dirname(CONFIG), "view.json"); // the table the scan screen was left on
|
|
14
16
|
/** The saved choice, minus anything this version doesn't offer; null on first run or an unreadable file */
|
|
15
17
|
export function loadConfig() {
|
|
16
18
|
try {
|
|
@@ -54,6 +56,22 @@ export function saveDockerWireGuard(wireguard) {
|
|
|
54
56
|
}
|
|
55
57
|
catch { } // not being able to remember isn't worth failing the scan
|
|
56
58
|
}
|
|
59
|
+
/** The table the last scan was left on; the ranking until the user switches */
|
|
60
|
+
export function loadView() {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(VIEW_FILE, "utf8")).view === "availability" ? "availability" : "ranking";
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return "ranking";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function saveView(view) {
|
|
69
|
+
try {
|
|
70
|
+
mkdirSync(dirname(VIEW_FILE), { recursive: true });
|
|
71
|
+
writeFileSync(VIEW_FILE, JSON.stringify({ view }) + "\n");
|
|
72
|
+
}
|
|
73
|
+
catch { } // not being able to remember isn't worth failing the scan
|
|
74
|
+
}
|
|
57
75
|
/** The last scan that found something; null when there's none or the file is unreadable */
|
|
58
76
|
export function loadLast() {
|
|
59
77
|
try {
|
package/dist/docker.js
CHANGED
|
@@ -29,7 +29,7 @@ const REGISTRIES = [...(process.env.WINDSCOUT_REGISTRY_MIRRORS?.split(",").map((
|
|
|
29
29
|
// openresolv: the app sets the tunnel's DNS through resolvconf; without it, DNS goes nowhere once connected.
|
|
30
30
|
// The CMD first checks the kernel (WSL's builds tun and parts of nftables as modules): no tun or nftables, no tunnel, so
|
|
31
31
|
// the container stops saying so instead of scanning. The helper's WireGuard routing also puts a fib rule in an inet table,
|
|
32
|
-
// which Docker Desktop's own kernel (macOS, Windows without WSL) can't run: WS_WIREGUARD=0 then, and
|
|
32
|
+
// which Docker Desktop's own kernel (macOS, Windows without WSL) can't run: WS_WIREGUARD=0 then, and a parallel scan skips it.
|
|
33
33
|
// No systemd in a container: start the helper (root) like its unit would; the worker starts the app once it has the session
|
|
34
34
|
const DOCKERFILE = `FROM ${BASE_TAG}
|
|
35
35
|
ENV LANG=C.UTF-8
|
|
@@ -107,8 +107,8 @@ export async function dockerState() {
|
|
|
107
107
|
const WORKER_MEMORY = 250e6; // bytes per container: Windscribe's app and helper, a tunnel and node take ~120 MB
|
|
108
108
|
// ponytail: past this, more containers mostly add load on the one uplink, and WireGuard keys on the account; raise if real scans say so
|
|
109
109
|
const MAX_WORKERS = 16;
|
|
110
|
-
/** Protocols the containers can't do, which a parallel scan
|
|
111
|
-
*
|
|
110
|
+
/** Protocols the containers can't do, which a parallel scan skips: IKEv2 (Windscribe's Linux app has none), and WireGuard
|
|
111
|
+
* when their kernel can't run its routing (see DOCKERFILE) */
|
|
112
112
|
export const notInContainers = (protocols, wireguard) => new Set(protocols.filter((p) => /^ikev2(:|$)/.test(p) || (!wireguard && /^wireguard(:|$)/.test(p))));
|
|
113
113
|
/** Containers for this many attempts, when docker can run at most `most`: one per attempt; past that, the fewest that need no
|
|
114
114
|
* more rounds (17 attempts, 16 at most: 9 containers make 2 rounds, as 16 would), since fewer log in sooner and register fewer keys */
|
package/dist/scan.js
CHANGED
|
@@ -29,7 +29,6 @@ export class Scan {
|
|
|
29
29
|
protocols;
|
|
30
30
|
options;
|
|
31
31
|
probers;
|
|
32
|
-
after;
|
|
33
32
|
results = [];
|
|
34
33
|
finished = []; // countries with every attempt done: the views refresh their tables per country
|
|
35
34
|
errors = []; // why slots stopped early
|
|
@@ -46,16 +45,13 @@ export class Scan {
|
|
|
46
45
|
idle = 0; // seconds spent paused
|
|
47
46
|
resume = () => { };
|
|
48
47
|
unpaused = Promise.resolve();
|
|
49
|
-
constructor(countries, protocols, options, probers = [probe]
|
|
48
|
+
constructor(countries, protocols, options, probers = [probe]) {
|
|
50
49
|
this.countries = countries;
|
|
51
50
|
this.protocols = protocols;
|
|
52
51
|
this.options = options;
|
|
53
52
|
this.probers = probers;
|
|
54
|
-
this.after = after;
|
|
55
53
|
this.total = this.attempts().length;
|
|
56
54
|
this.slots = probers.map(() => ({ current: null, step: "", attemptStart: now() }));
|
|
57
|
-
if (after)
|
|
58
|
-
this.slots.push({ current: null, step: "", attemptStart: now(), name: after.name });
|
|
59
55
|
}
|
|
60
56
|
/** Seconds spent scanning, pauses not counted */
|
|
61
57
|
elapsed() {
|
|
@@ -87,15 +83,12 @@ export class Scan {
|
|
|
87
83
|
const left = new Map(); // attempts to go, per country
|
|
88
84
|
for (const { country } of queue)
|
|
89
85
|
left.set(country, (left.get(country) ?? 0) + 1);
|
|
90
|
-
|
|
91
|
-
await Promise.all(this.probers.map((prober, i) => this.work(prober, this.slots[i],
|
|
92
|
-
// then the rest, also what slots whose container stopped left over
|
|
93
|
-
if (this.after)
|
|
94
|
-
await this.work(this.after.prober, this.slots[this.probers.length], () => true, queue, left, hooks);
|
|
86
|
+
// when every slot's container stops, what they left isn't tried: scan.errors says why
|
|
87
|
+
await Promise.all(this.probers.map((prober, i) => this.work(prober, this.slots[i], queue, left, hooks)));
|
|
95
88
|
return this.results;
|
|
96
89
|
}
|
|
97
|
-
/** One slot: takes the attempts
|
|
98
|
-
async work(prober, slot,
|
|
90
|
+
/** One slot: takes the attempts from the queue, one at a time, until there are none */
|
|
91
|
+
async work(prober, slot, queue, left, hooks) {
|
|
99
92
|
const { timeout, speedTest, stopAtFirst = false } = this.options;
|
|
100
93
|
for (;;) {
|
|
101
94
|
while (this.paused && !this.stopping) {
|
|
@@ -105,10 +98,9 @@ export class Scan {
|
|
|
105
98
|
}
|
|
106
99
|
await this.unpaused;
|
|
107
100
|
}
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
101
|
+
const next = this.stopping ? undefined : queue.shift();
|
|
102
|
+
if (!next)
|
|
110
103
|
return;
|
|
111
|
-
const [next] = queue.splice(i, 1);
|
|
112
104
|
const { country, protocol } = next;
|
|
113
105
|
Object.assign(slot, { current: next, step: "", attemptStart: now() });
|
|
114
106
|
hooks.attempt?.(country, protocol);
|
package/dist/tui/picker.js
CHANGED
|
@@ -41,12 +41,15 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
41
41
|
};
|
|
42
42
|
const ports = (p) => (allPorts && portLists?.get(p)?.length) || 1; // attempts per country for the protocol
|
|
43
43
|
const perCountry = (ps) => [...ps].reduce((n, p) => n + ports(p), 0);
|
|
44
|
-
|
|
44
|
+
const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
|
|
45
|
+
// what start scans, in its order: as the CLI plans it, so the estimate counts the same attempts. A parallel scan is the
|
|
46
|
+
// containers' alone: the protocols they can't run are skipped
|
|
45
47
|
const todo = () => {
|
|
46
48
|
const ps = PROTOCOLS.filter((p) => protos.has(p));
|
|
47
|
-
|
|
49
|
+
const all = plan(order, COUNTRIES.filter((c) => selected.has(c)), allPorts && portLists ? withPorts(ps, portLists) : ps, last?.results, portLists ?? undefined);
|
|
50
|
+
const cant = notInContainers(docker ? all.map((a) => a.protocol) : [], wireguard);
|
|
51
|
+
return { list: all.filter((a) => !cant.has(a.protocol)), skipped: [...new Set([...cant].map((p) => p.split(":")[0]))] };
|
|
48
52
|
};
|
|
49
|
-
const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
|
|
50
53
|
enterScreen();
|
|
51
54
|
return new Promise((resolve) => {
|
|
52
55
|
const render = () => {
|
|
@@ -54,24 +57,24 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
54
57
|
return;
|
|
55
58
|
const rows = out.rows || 24;
|
|
56
59
|
const version = versionLines(true);
|
|
57
|
-
const matches = visible();
|
|
58
|
-
const height = Math.max(3, rows - 24 - version.length);
|
|
59
|
-
cursor = Math.max(0, Math.min(cursor, matches.length - 1));
|
|
60
|
-
if (cursor < top)
|
|
61
|
-
top = cursor;
|
|
62
|
-
if (cursor >= top + height)
|
|
63
|
-
top = cursor - height + 1;
|
|
64
|
-
top = Math.max(0, Math.min(top, matches.length - height));
|
|
65
60
|
const off = order === "last-only"; // only the saved connections: the countries, protocols and ports are skipped
|
|
66
61
|
const missing = needsLast(order) && !last; // a type by the last result, and no saved scan: start is off
|
|
67
62
|
timeout = Number(seconds);
|
|
68
|
-
const list = todo(), n = list.length;
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
const { list, skipped } = todo(), n = list.length;
|
|
64
|
+
// only the last results lists the countries it scans, all selected; the user's own selection is back with the other types
|
|
65
|
+
const matches = off ? [...new Set(list.map((a) => a.country))] : visible();
|
|
66
|
+
const height = Math.max(3, rows - 24 - version.length);
|
|
67
|
+
if (!off) { // so the selection's list keeps its cursor and scroll for then
|
|
68
|
+
cursor = Math.max(0, Math.min(cursor, matches.length - 1));
|
|
69
|
+
if (cursor < top)
|
|
70
|
+
top = cursor;
|
|
71
|
+
if (cursor >= top + height)
|
|
72
|
+
top = cursor - height + 1;
|
|
73
|
+
top = Math.max(0, Math.min(top, matches.length - height));
|
|
74
|
+
}
|
|
75
|
+
const from = off ? 0 : top; // the first row shown
|
|
76
|
+
const parallel = docker && dockerAt === "ready" && n > 0;
|
|
77
|
+
const slots = parallel ? workerCount(n) : 1;
|
|
75
78
|
const login = parallel ? loginSeconds(slots) : 0; // the containers log in before the scan starts
|
|
76
79
|
const protoLine = PROTOCOLS.map((p, i) => `${dim(under(String(i + 1)))} ${protos.has(p) ? (off ? dim : green)("● " + p) : dim("○ " + p)}`).join(" ");
|
|
77
80
|
const listed = portLists && [...portLists.values()].some((l) => l.length), each = perCountry(protos);
|
|
@@ -95,8 +98,8 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
95
98
|
const dockerLabel = "parallel scan in docker containers";
|
|
96
99
|
const dockerLine = `${dim(under("d"))} ${docker ? green("● " + dockerLabel) : dim("○ " + dockerLabel)} ${dim(!docker
|
|
97
100
|
? "off: one attempt at a time"
|
|
98
|
-
: dockerAt === "ready" ? `${slots} at a time
|
|
99
|
-
|
|
101
|
+
: (dockerAt === "ready" ? `${slots} at a time` : dockerAt ? "docker isn't ready: start sets it up" : "checking docker…")
|
|
102
|
+
+ (skipped.length ? ` · ${skipped.join(" and ")} skipped` : ""))}`;
|
|
100
103
|
const saves = last ? plan("last-only", [], [], last.results, portLists ?? undefined).length : 0; // the saved connections, each once
|
|
101
104
|
const firsts = list.filter((a) => a.saved).length; // those this scan tries first
|
|
102
105
|
const orderLine = `${dim(under("o"))} scanning type ${bold(ORDER_NAMES[order])} ${dim(missing
|
|
@@ -106,26 +109,26 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
106
109
|
: order === "last-first" ? `${firsts} of ${saves} saved selected · ${ago(last.at)}`
|
|
107
110
|
: `${saves} saved working connection${saves === 1 ? "" : "s"} · ${ago(last.at)}`)}`;
|
|
108
111
|
const stopLabel = "stop at the first working connection";
|
|
109
|
-
// the best saved connection goes first
|
|
110
|
-
const lead = list[0]?.saved
|
|
112
|
+
// the best saved connection goes first: when it still holds up, the scan ends there
|
|
113
|
+
const lead = list[0]?.saved ? list[0] : null;
|
|
111
114
|
const stopLine = `${dim(under("w"))} ${stopAtFirst ? green("● " + stopLabel) : dim("○ " + stopLabel)} ${dim(!stopAtFirst
|
|
112
115
|
? "off: tries every attempt"
|
|
113
116
|
: lead ? `≈ ${duration(login + STEADY_SECONDS)} if ${lead.country} ${lead.protocol} still holds up`
|
|
114
117
|
: "after a speed and stability check")}`;
|
|
115
118
|
const search = off
|
|
116
|
-
? yellow("countries
|
|
119
|
+
? yellow("the last results' countries: only these are scanned")
|
|
117
120
|
: searching ? `${cyan("🔍")} ${bold(query)}${cyan("▏")}`
|
|
118
121
|
: query ? `🔍 ${query}` : dim("🔍 press / to search");
|
|
119
|
-
const shown = matches.slice(
|
|
120
|
-
const range = matches.length > height ? `${
|
|
122
|
+
const shown = matches.slice(from, from + height);
|
|
123
|
+
const range = matches.length > height ? `${from + 1}–${from + shown.length} of ${matches.length}` : `${matches.length} countries`;
|
|
121
124
|
const body = shown.map((code, j) => {
|
|
122
125
|
const current = !off && top + j === cursor;
|
|
123
|
-
const mark = selected.has(code) ? (off ? dim : green)("●") : dim("○");
|
|
126
|
+
const mark = off || selected.has(code) ? (off ? dim : green)("●") : dim("○");
|
|
124
127
|
const name = `${flag(code)} ${countryName(code)}`;
|
|
125
128
|
return [`${current ? cyan("❯") : " "} ${mark}`, current ? bold(code) : dim(code), current ? bold(cyan(name)) : off ? dim(name) : name];
|
|
126
129
|
});
|
|
127
130
|
if (!body.length)
|
|
128
|
-
body.push([" ", "", dim("no country matches")]);
|
|
131
|
+
body.push([" ", "", dim(off ? "no saved scan yet" : "no country matches")]);
|
|
129
132
|
const buttons = [
|
|
130
133
|
["start", starting < 0 ? "▶ Start" : `${spin(starting++)} Starting…`],
|
|
131
134
|
["all", under("A") + (query ? "ll shown" : "ll")],
|
|
@@ -138,10 +141,10 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
138
141
|
const width = vw(table[0]);
|
|
139
142
|
const head = box(settings, {
|
|
140
143
|
title: bold(cyan("WindScout")),
|
|
141
|
-
right: `${(off ? dim : bold)(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
|
|
144
|
+
right: `${(off ? dim : bold)(String(off ? matches.length : selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
|
|
142
145
|
width: width - 4,
|
|
143
146
|
});
|
|
144
|
-
const total = login + estimate(
|
|
147
|
+
const total = login + estimate(n, timeout, slots);
|
|
145
148
|
const costs = dim(`(~${attemptSeconds(timeout)}s each${slots > 1 ? `, ${slots} at a time` : ""}${login ? `, + ~${duration(login)} login` : ""})`);
|
|
146
149
|
const lines = [
|
|
147
150
|
...head,
|
|
@@ -151,7 +154,8 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
151
154
|
"",
|
|
152
155
|
centerIn(missing
|
|
153
156
|
? yellow("no saved scan yet, so start is off: press o for another scanning type")
|
|
154
|
-
: !n ? yellow(
|
|
157
|
+
: !n ? yellow(skipped.length ? `the docker containers can't run ${skipped.join(" or ")}: d turns docker off`
|
|
158
|
+
: `select at least one country and one protocol${last ? ", or o for only the last results" : ""}`)
|
|
155
159
|
: stopAtFirst // it can end at any attempt: this is when none holds up
|
|
156
160
|
? `up to ${bold(String(n))} attempts · at most ${bold("≈ " + duration(total))} ${costs}`
|
|
157
161
|
: `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(total))} ${costs}`, width),
|
|
@@ -207,7 +211,7 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
207
211
|
const start = () => {
|
|
208
212
|
if (needsLast(order) && !last)
|
|
209
213
|
return false; // start is off: the warning under the table says why
|
|
210
|
-
if (!todo().length) {
|
|
214
|
+
if (!todo().list.length) {
|
|
211
215
|
message = "nothing to scan yet";
|
|
212
216
|
return false;
|
|
213
217
|
}
|
package/dist/tui/progress.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadView, saveView } from "../config.js";
|
|
1
2
|
import { sortOk } from "../probe.js";
|
|
2
3
|
import { attemptSeconds } from "../scan.js";
|
|
3
4
|
import { duration, now } from "../time.js";
|
|
@@ -8,13 +9,13 @@ import { matrix, resultTable } from "./tables.js";
|
|
|
8
9
|
import { draw, enterScreen, input, leaveScreen, out, repoLink } from "./terminal.js";
|
|
9
10
|
import { bold, cyan, dim, green, red, vw, yellow } from "./text.js";
|
|
10
11
|
import { bar, box, centerIn, spin } from "./widgets.js";
|
|
11
|
-
/** Progress bar, current attempt, results so far. tab switches the table, p pauses, q stops (twice: quits).
|
|
12
|
+
/** Progress bar, current attempt, results so far. tab switches the table (the next scans start on it), p pauses, q stops (twice: quits).
|
|
12
13
|
* blocking: we turned the firewall on for the scan */
|
|
13
14
|
export function showProgress(scan, blocking) {
|
|
14
15
|
const { protocols, options: { timeout } } = scan;
|
|
15
16
|
const speedTest = scan.options.speedTest || Boolean(scan.options.stopAtFirst); // stop at the first measures speed too
|
|
16
17
|
let quitting = false; // q pressed: a second one quits right away
|
|
17
|
-
let view =
|
|
18
|
+
let view = loadView(); // tab switches, and saves it for the next scans
|
|
18
19
|
let frame = 0;
|
|
19
20
|
const render = () => {
|
|
20
21
|
const rows = out.rows || 24;
|
|
@@ -60,8 +61,8 @@ export function showProgress(scan, blocking) {
|
|
|
60
61
|
const pausing = paused && !waiting && frame % 8 < 5;
|
|
61
62
|
const slotLine = (s) => {
|
|
62
63
|
if (!s.current)
|
|
63
|
-
return s.step ? ` ${red("✗")} ${dim(clip(s.step, inner - 3))}` : dim(
|
|
64
|
-
const head = ` ${cyan(spinner)} ${
|
|
64
|
+
return s.step ? ` ${red("✗")} ${dim(clip(s.step, inner - 3))}` : dim(" · idle");
|
|
65
|
+
const head = ` ${cyan(spinner)} ${bold(attempt(s))} `, time = ` · ${duration(now() - s.attemptStart)}`;
|
|
65
66
|
return head + dim(clip(s.step, inner - vw(head) - time.length) + time);
|
|
66
67
|
};
|
|
67
68
|
const panel = box([
|
|
@@ -98,6 +99,7 @@ export function showProgress(scan, blocking) {
|
|
|
98
99
|
input.on("keypress", async (_str, key = {}) => {
|
|
99
100
|
if (key.name === "tab") {
|
|
100
101
|
view = view === "ranking" ? "availability" : "ranking";
|
|
102
|
+
saveView(view);
|
|
101
103
|
render();
|
|
102
104
|
}
|
|
103
105
|
else if (key.name === "p" || key.name === "space") {
|