windscout 0.7.0 → 0.9.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 +89 -16
- package/dist/background.js +131 -0
- package/dist/cli.js +180 -121
- package/dist/config.js +44 -5
- package/dist/docker.js +123 -25
- package/dist/measure.js +12 -0
- package/dist/probe.js +50 -24
- package/dist/scan.js +8 -16
- package/dist/time.js +5 -0
- package/dist/tui/connect.js +29 -0
- package/dist/tui/picker.js +80 -63
- package/dist/tui/progress.js +6 -4
- package/dist/tui/report.js +14 -4
- package/dist/tui/results.js +454 -0
- package/dist/tui/text.js +19 -0
- package/dist/watch.js +454 -0
- package/dist/windscribe.js +58 -10
- package/dist/worker.js +8 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,30 +2,41 @@
|
|
|
2
2
|
// WindScout: find which Windscribe locations and protocols actually work from here.
|
|
3
3
|
// Drives the official windscribe-cli: connects to each country/protocol pair,
|
|
4
4
|
// measures latency and download speed through the tunnel, prints a ranked table.
|
|
5
|
+
// In a terminal, the results then stay on screen (tui/results.ts): connect, test again, and stay connected.
|
|
5
6
|
import { createInterface } from "node:readline";
|
|
6
7
|
import { spawnSync } from "node:child_process";
|
|
7
8
|
import { parseArgs } from "node:util";
|
|
8
|
-
import {
|
|
9
|
+
import { dockerChecker } from "./background.js";
|
|
10
|
+
import { CONFIG, DEFAULT_HOLD, DEFAULT_TIMEOUT, forgetLogin, LAST_FILE, loadConfig, loadDockerWireGuard, loadLast, MIN_HOLD, MIN_TIMEOUT, saveDockerWireGuard, saveLast, SPEED_TEST } from "./config.js";
|
|
9
11
|
import { notInContainers, stopWorkers } from "./docker.js";
|
|
10
|
-
import {
|
|
12
|
+
import { disconnect, MIN_MBPS, probe } from "./probe.js";
|
|
11
13
|
import { ORDERS, plan, Scan } from "./scan.js";
|
|
14
|
+
import { connectFrom } from "./tui/connect.js";
|
|
12
15
|
import { startParallel } from "./tui/docker.js";
|
|
13
16
|
import { pick } from "./tui/picker.js";
|
|
14
17
|
import { logProgress, showProgress } from "./tui/progress.js";
|
|
15
|
-
import {
|
|
18
|
+
import { report } from "./tui/report.js";
|
|
19
|
+
import { showResults } from "./tui/results.js";
|
|
16
20
|
import { leaveScreen, out } from "./tui/terminal.js";
|
|
17
|
-
import {
|
|
18
|
-
import { duration
|
|
21
|
+
import { green, yellow } from "./tui/text.js";
|
|
22
|
+
import { duration } from "./time.js";
|
|
19
23
|
import { canUpdate, checkUpdate, runUpdate, updateNotice, versionLines } from "./update.js";
|
|
20
|
-
import { blockInternet, CLI, isFirewallOn, protocolPorts, PROTOCOLS, restoreFirewall, waitForLogin,
|
|
24
|
+
import { blockInternet, CLI, isFirewallOn, protocolPorts, PROTOCOLS, restoreFirewall, waitForLogin, withPorts } from "./windscribe.js";
|
|
21
25
|
import { worker } from "./worker.js";
|
|
26
|
+
import { CHECK_EVERY } from "./watch.js";
|
|
22
27
|
const HELP = `Usage: windscout [options]
|
|
23
28
|
windscout connect [-t SEC]
|
|
24
29
|
|
|
25
30
|
Find which Windscribe locations and protocols work from your network, ranked by ${SPEED_TEST ? "speed" : "latency"}.
|
|
26
31
|
Without -c it opens an interactive country picker (your selection is remembered).
|
|
32
|
+
In a terminal, the results then stay on screen until you quit: connect to one (the screen shows
|
|
33
|
+
where you're connected, live), test them all again for speed or latency (what doesn't connect
|
|
34
|
+
goes to the end of the list), and watch the connection: when it drops, windscout connects you to
|
|
35
|
+
the next best. While watching, docker containers can check the list every ${duration(CHECK_EVERY)} without touching
|
|
36
|
+
your VPN (b there; latency only). n there starts a new scan; q quits and leaves the VPN as it is.
|
|
27
37
|
Each scan's working connections are saved: the scanning type (-o, or o in the picker) can try
|
|
28
|
-
them first or only them, and windscout connect connects to the best of them that still works
|
|
38
|
+
them first or only them, and windscout connect connects to the best of them that still works
|
|
39
|
+
(in a terminal, on the results screen).
|
|
29
40
|
|
|
30
41
|
Options:
|
|
31
42
|
-c, --countries LIST comma-separated ISO codes, city names or nicknames (skips the picker)
|
|
@@ -38,7 +49,9 @@ ${SPEED_TEST ? " -n, --no-speed connect-only: skip the download test, ra
|
|
|
38
49
|
result's working connections among them first, then the rest) or last-only (only those,
|
|
39
50
|
whatever -c and -p say; needs no -c)
|
|
40
51
|
-w, --stop-at-first stop at the first working connection that holds up: a speed test (even with -n) of at least
|
|
41
|
-
${MIN_MBPS} Mbps,
|
|
52
|
+
${MIN_MBPS} Mbps, staying connected with internet through it (-H), a speed test again, and a reconnect
|
|
53
|
+
-H, --hold SEC with -w: seconds each working connection has to stay connected, at least ${MIN_HOLD}
|
|
54
|
+
(default: saved, or ${DEFAULT_HOLD})
|
|
42
55
|
-d, --docker scan in parallel, in docker containers (one per attempt, up to 16, as docker's CPUs and memory allow)
|
|
43
56
|
-v, --version show the installed version (and offer to update if a newer one is out)
|
|
44
57
|
-h, --help show this help
|
|
@@ -49,41 +62,18 @@ Examples: windscout -c DE,NL,TR -p stealth,tcp:443
|
|
|
49
62
|
windscout connect`;
|
|
50
63
|
// "DE, NL,,TR" → ["DE", "NL", "TR"]
|
|
51
64
|
const list = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
52
|
-
|
|
65
|
+
/** The typed answer; one of instant is taken as soon as it's the whole answer, without enter */
|
|
66
|
+
const ask = (question, instant = []) => new Promise((resolve) => {
|
|
53
67
|
const rl = createInterface({ input: process.stdin, output: out });
|
|
68
|
+
// runs after readline's own keypress listener, so rl.line has the key in it
|
|
69
|
+
const onKey = () => instant.includes(rl.line.toLowerCase()) && (out.write("\n"), resolve(rl.line), rl.close());
|
|
70
|
+
process.stdin.on("keypress", onKey);
|
|
54
71
|
rl.on("SIGINT", () => rl.close()); // Ctrl-C / Ctrl-D: no answer, null
|
|
55
|
-
rl.on("close", () => resolve(null));
|
|
72
|
+
rl.on("close", () => (process.stdin.off("keypress", onKey), resolve(null)));
|
|
56
73
|
rl.question(question, (a) => (resolve(a.trim()), rl.close()));
|
|
57
74
|
});
|
|
58
|
-
/** Runs a result's connect command and checks it as the scan does: connected, on its port, with internet through it.
|
|
59
|
-
* True once it is; otherwise disconnects (and waits until the app has), so the next connect isn't swallowed */
|
|
60
|
-
async function connectTo(r, timeout) {
|
|
61
|
-
console.log(cyan(command(r)));
|
|
62
|
-
const t = now(), { status, location } = await attempt(target(r), r.protocol, { timeout, speedTest: false });
|
|
63
|
-
if (status === "ok")
|
|
64
|
-
return console.log(green(`Connected: ${location}`)), true;
|
|
65
|
-
console.log(yellow(`${status} after ${duration(now() - t)}`));
|
|
66
|
-
await disconnect();
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
/** Asks which numbered connection to make (enter: the best), then runs its command and waits for it to connect */
|
|
70
|
-
async function offerConnect(choices, timeout) {
|
|
71
|
-
const others = choices.length > 2 ? `, 1-${choices.length - 1} = other` : choices.length > 1 ? ", 1 = other" : "";
|
|
72
|
-
for (;;) {
|
|
73
|
-
const a = await ask(`\nconnect to # (enter or 0 = best${others}, q = quit): `);
|
|
74
|
-
if (a === null || /^q(uit)?$/i.test(a))
|
|
75
|
-
return;
|
|
76
|
-
const r = a === "" ? choices[0] : /^\d+$/.test(a) ? choices[Number(a)] : undefined;
|
|
77
|
-
if (!r) {
|
|
78
|
-
console.log(yellow(`no #${a} in the tables`));
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
await connectTo(r, timeout);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
75
|
const RESCAN_HINT = "rescan them with windscout -o last-only, or start a new scan";
|
|
86
|
-
/** windscout connect: the last result's connections, best first, until one connects */
|
|
76
|
+
/** windscout connect without a terminal: the last result's connections, best first, until one connects */
|
|
87
77
|
async function connectLast(timeout) {
|
|
88
78
|
const last = loadLast();
|
|
89
79
|
if (!last) {
|
|
@@ -93,13 +83,11 @@ async function connectLast(timeout) {
|
|
|
93
83
|
}
|
|
94
84
|
await waitForLogin();
|
|
95
85
|
await disconnect(); // a connection already up would read as the first one connecting
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
const i = await connectFrom(last.results, 0, timeout);
|
|
87
|
+
if (i < 0) {
|
|
88
|
+
console.log(yellow(`\nnone of the last result's ${last.results.length} connection${last.results.length === 1 ? "" : "s"} works now: ${RESCAN_HINT}`));
|
|
89
|
+
process.exitCode = 1;
|
|
100
90
|
}
|
|
101
|
-
console.log(yellow(`\nnone of the last result's ${last.results.length} connection${last.results.length === 1 ? "" : "s"} works now: ${RESCAN_HINT}`));
|
|
102
|
-
process.exitCode = 1;
|
|
103
91
|
}
|
|
104
92
|
/** u in the picker: update, then run the new version in this one's place, with the same arguments */
|
|
105
93
|
function updateAndRestart() {
|
|
@@ -112,63 +100,12 @@ function updateAndRestart() {
|
|
|
112
100
|
console.log(green("updated · restarting windscout"));
|
|
113
101
|
process.exitCode = spawnSync(process.execPath, [...process.execArgv, ...process.argv.slice(1)], { stdio: "inherit" }).status ?? 1;
|
|
114
102
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
options: {
|
|
119
|
-
countries: { type: "string", short: "c" },
|
|
120
|
-
protocols: { type: "string", short: "p" },
|
|
121
|
-
"all-ports": { type: "boolean", short: "P", default: false },
|
|
122
|
-
timeout: { type: "string", short: "t" },
|
|
123
|
-
all: { type: "boolean", short: "a", default: false },
|
|
124
|
-
help: { type: "boolean", short: "h", default: false },
|
|
125
|
-
version: { type: "boolean", short: "v", default: false },
|
|
126
|
-
"no-speed": { type: "boolean", short: "n", default: false },
|
|
127
|
-
firewall: { type: "boolean", short: "f", default: false },
|
|
128
|
-
order: { type: "string", short: "o" },
|
|
129
|
-
"stop-at-first": { type: "boolean", short: "w", default: false },
|
|
130
|
-
random: { type: "boolean", short: "r", default: false }, // from before scanning types: -o random
|
|
131
|
-
docker: { type: "boolean", short: "d", default: false },
|
|
132
|
-
worker: { type: "boolean", default: false }, // in a parallel scan's container
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
if (a.worker) {
|
|
136
|
-
await worker();
|
|
137
|
-
process.exit(); // its stdin may still be open: its app didn't log in
|
|
138
|
-
}
|
|
139
|
-
forgetLogin();
|
|
140
|
-
if (a.help)
|
|
141
|
-
return console.log(HELP);
|
|
142
|
-
const [subcommand, ...extra] = positionals;
|
|
143
|
-
if (subcommand !== undefined && (subcommand !== "connect" || extra.length))
|
|
144
|
-
throw new Error(`unknown command '${positionals.join(" ")}': see --help`);
|
|
145
|
-
if (subcommand === "connect" && Object.entries(a).some(([k, v]) => k !== "timeout" && v))
|
|
146
|
-
throw new Error("windscout connect takes only -t: see --help");
|
|
147
|
-
checkUpdate(); // runs while the app starts and the user picks, before the VPN starts switching
|
|
148
|
-
if (a.version) {
|
|
149
|
-
await checkUpdate();
|
|
150
|
-
console.log(versionLines().join("\n"));
|
|
151
|
-
if (canUpdate() && process.stdin.isTTY && out.isTTY && /^y(es)?$/i.test((await ask("update now? (y/n) ")) ?? ""))
|
|
152
|
-
process.exitCode = runUpdate();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
let timeout = a.timeout ? Number(a.timeout) : undefined;
|
|
156
|
-
if (timeout !== undefined && !(timeout >= MIN_TIMEOUT))
|
|
157
|
-
throw new Error(`--timeout must be at least ${MIN_TIMEOUT} seconds`);
|
|
158
|
-
if (a.order !== undefined && !ORDERS.includes(a.order))
|
|
159
|
-
throw new Error(`--order must be one of: ${ORDERS.join(", ")}`);
|
|
160
|
-
if (subcommand === "connect")
|
|
161
|
-
return connectLast(timeout ?? loadConfig()?.timeout ?? DEFAULT_TIMEOUT);
|
|
103
|
+
/** One scan: the picker (unless -c), then the scan, its report, and its working connections saved. Resolves to them, best
|
|
104
|
+
* first, and the timeout it used; null when the picker was left, "update" when u was pressed there */
|
|
105
|
+
async function scanOnce(f, tty) {
|
|
162
106
|
const status = await waitForLogin();
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
let protocols = a.protocols ? list(a.protocols) : undefined;
|
|
166
|
-
let speedTest = SPEED_TEST && !a["no-speed"];
|
|
167
|
-
let firewall = a.firewall;
|
|
168
|
-
let order = (a.order ?? (a.random ? "random" : undefined));
|
|
169
|
-
let stopAtFirst = a["stop-at-first"];
|
|
170
|
-
let allPorts = a["all-ports"];
|
|
171
|
-
let docker = a.docker;
|
|
107
|
+
let { countries, protocols, speedTest, firewall, order, stopAtFirst, allPorts, docker, timeout, hold } = f;
|
|
108
|
+
speedTest &&= SPEED_TEST;
|
|
172
109
|
const last = loadLast();
|
|
173
110
|
// only the last results needs no countries: without a terminal, it goes without the picker
|
|
174
111
|
if (!countries && !(order === "last-only" && !tty)) {
|
|
@@ -184,20 +121,20 @@ async function main() {
|
|
|
184
121
|
firewall: firewall || isFirewallOn(status) || (saved?.firewall ?? false),
|
|
185
122
|
order: order ?? saved?.order ?? "default",
|
|
186
123
|
stopAtFirst: stopAtFirst || (saved?.stopAtFirst ?? false),
|
|
124
|
+
hold: hold ?? saved?.hold ?? DEFAULT_HOLD,
|
|
187
125
|
allPorts: allPorts || (saved?.allPorts ?? false),
|
|
188
126
|
docker: docker || (saved?.docker ?? false),
|
|
189
127
|
last,
|
|
190
128
|
});
|
|
191
|
-
if (!choice)
|
|
192
|
-
return;
|
|
193
|
-
if (choice === "update")
|
|
194
|
-
return updateAndRestart();
|
|
129
|
+
if (!choice || choice === "update")
|
|
130
|
+
return choice;
|
|
195
131
|
countries = choice.countries;
|
|
196
132
|
protocols ??= choice.protocols;
|
|
197
133
|
speedTest = choice.speed;
|
|
198
134
|
firewall = choice.firewall;
|
|
199
135
|
order = choice.order;
|
|
200
136
|
stopAtFirst = choice.stopAtFirst;
|
|
137
|
+
hold = choice.hold;
|
|
201
138
|
allPorts = choice.allPorts;
|
|
202
139
|
timeout = choice.timeout;
|
|
203
140
|
docker = choice.docker;
|
|
@@ -205,6 +142,7 @@ async function main() {
|
|
|
205
142
|
countries ??= [];
|
|
206
143
|
protocols ??= PROTOCOLS;
|
|
207
144
|
timeout ??= DEFAULT_TIMEOUT;
|
|
145
|
+
hold ??= DEFAULT_HOLD;
|
|
208
146
|
order ??= "default";
|
|
209
147
|
const byLast = order === "last-first" || order === "last-only"; // a type by the last result: it needs a saved scan
|
|
210
148
|
if (byLast && !last)
|
|
@@ -212,49 +150,170 @@ async function main() {
|
|
|
212
150
|
const fromLast = byLast ? last : null; // the saved connections this scan uses
|
|
213
151
|
// the picker asked for the ports already: waiting for that keeps windscribe-cli free for the scan.
|
|
214
152
|
// The saved connections need them too, to tell a saved "tcp:443" from tcp on its default port
|
|
215
|
-
const lists = allPorts || !
|
|
153
|
+
const lists = allPorts || !f.countries || fromLast ? await protocolPorts() : undefined;
|
|
216
154
|
if (allPorts && lists)
|
|
217
155
|
protocols = withPorts(protocols, lists);
|
|
218
156
|
const attempts = plan(order, countries, protocols, fromLast?.results, lists);
|
|
219
157
|
// the tables' columns: the protocols asked for, and the saved connections' own (only those, when only they are scanned)
|
|
220
158
|
const scanned = [...new Set([...(order === "last-only" ? [] : protocols), ...attempts.map((x) => x.protocol)])];
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const inContainers = attempts.filter((x) => !
|
|
224
|
-
|
|
225
|
-
|
|
159
|
+
// a parallel scan is the containers' alone: the protocols they can't run (IKEv2, WireGuard on some kernels) aren't tried
|
|
160
|
+
let probers = [probe], skipped = new Set();
|
|
161
|
+
const inContainers = () => attempts.filter((x) => !skipped.has(x.protocol));
|
|
162
|
+
const nothingLeft = () => new Error(`the docker containers can't run ${[...skipped].join(" or ")}, and nothing else is selected:`
|
|
163
|
+
+ " scan without --docker (d in the picker)");
|
|
164
|
+
if (docker && attempts.length) {
|
|
165
|
+
skipped = notInContainers(scanned, loadDockerWireGuard()); // as far as the last parallel scan knows
|
|
166
|
+
if (!inContainers().length)
|
|
167
|
+
throw nothingLeft();
|
|
168
|
+
const started = await startParallel(inContainers().length);
|
|
226
169
|
if (!started)
|
|
227
|
-
return leaveScreen();
|
|
170
|
+
return leaveScreen(), null;
|
|
228
171
|
saveDockerWireGuard(started.wireguard);
|
|
229
172
|
probers = started.probers;
|
|
230
|
-
|
|
231
|
-
|
|
173
|
+
skipped = notInContainers(scanned, started.wireguard); // their kernel may not run WireGuard after all, or may now
|
|
174
|
+
if (!inContainers().length) {
|
|
175
|
+
await stopWorkers();
|
|
176
|
+
throw nothingLeft();
|
|
177
|
+
}
|
|
232
178
|
}
|
|
233
|
-
const scan = new Scan(countries, scanned, { timeout, speedTest, only:
|
|
179
|
+
const scan = new Scan(countries, scanned.filter((p) => !skipped.has(p)), { timeout, speedTest, only: inContainers(), stopAtFirst: stopAtFirst ? hold : 0 }, probers);
|
|
180
|
+
// the scan switches the VPN anyway: one still up (from before, or the results screen) would read as its first attempt
|
|
181
|
+
// connecting, and with containers, it would carry their traffic too
|
|
182
|
+
if (scan.total)
|
|
183
|
+
await disconnect();
|
|
234
184
|
// not with containers: their traffic goes out through this computer, and the firewall would block it
|
|
235
|
-
const blocking = firewall && !
|
|
185
|
+
const blocking = firewall && !docker && (await blockInternet());
|
|
236
186
|
const view = tty ? showProgress(scan, blocking) : logProgress(scan);
|
|
237
187
|
await scan.run(view);
|
|
238
|
-
if (
|
|
188
|
+
if (docker)
|
|
239
189
|
await stopWorkers();
|
|
240
190
|
await restoreFirewall();
|
|
241
191
|
view.close();
|
|
242
192
|
for (const e of new Set(scan.errors))
|
|
243
193
|
console.error(yellow(e));
|
|
194
|
+
if (skipped.size)
|
|
195
|
+
console.error(yellow(`not tried: ${[...skipped].join(", ")}, which the docker containers can't run (a scan without --docker can)`));
|
|
244
196
|
if (scan.found)
|
|
245
|
-
console.log(green(
|
|
197
|
+
console.log(green(`stopped at the first connection that held up: a speed test, ${duration(hold)} connected, a speed test again and a reconnect`));
|
|
246
198
|
else if (scan.stopping)
|
|
247
199
|
console.log(yellow("interrupted, showing partial results"));
|
|
248
|
-
|
|
200
|
+
// a slower one is never stopped at, so it isn't checked
|
|
201
|
+
else if (stopAtFirst && scan.results.some((r) => r.status === "ok"))
|
|
202
|
+
console.log(yellow(`none held up at ${MIN_MBPS} Mbps or more: the connections below are slower, and weren't checked for stability`));
|
|
203
|
+
const results = report(scan.results, scan.protocols, { all: f.all, speedTest: speedTest || stopAtFirst }); // stop at the first measures speed
|
|
249
204
|
// its working connections replace the saved ones it tried, so what stopped working drops out; the saved ones it didn't try
|
|
250
205
|
// (other countries, or what a stopped scan didn't get to) stay, after its own; a scan where nothing worked keeps the last
|
|
251
206
|
const tried = new Set(scan.results.filter((r) => r.status !== "interrupted").map((r) => `${r.query} ${r.protocol}`));
|
|
252
|
-
saveLast(
|
|
207
|
+
saveLast(results, undefined, tried);
|
|
253
208
|
await checkUpdate();
|
|
254
209
|
if (updateNotice())
|
|
255
210
|
console.error("\n" + versionLines().join("\n"));
|
|
256
|
-
|
|
257
|
-
|
|
211
|
+
return { results, timeout };
|
|
212
|
+
}
|
|
213
|
+
async function main() {
|
|
214
|
+
const { values: a, positionals } = parseArgs({
|
|
215
|
+
allowPositionals: true,
|
|
216
|
+
options: {
|
|
217
|
+
countries: { type: "string", short: "c" },
|
|
218
|
+
protocols: { type: "string", short: "p" },
|
|
219
|
+
"all-ports": { type: "boolean", short: "P", default: false },
|
|
220
|
+
timeout: { type: "string", short: "t" },
|
|
221
|
+
all: { type: "boolean", short: "a", default: false },
|
|
222
|
+
help: { type: "boolean", short: "h", default: false },
|
|
223
|
+
version: { type: "boolean", short: "v", default: false },
|
|
224
|
+
"no-speed": { type: "boolean", short: "n", default: false },
|
|
225
|
+
firewall: { type: "boolean", short: "f", default: false },
|
|
226
|
+
order: { type: "string", short: "o" },
|
|
227
|
+
"stop-at-first": { type: "boolean", short: "w", default: false },
|
|
228
|
+
hold: { type: "string", short: "H" },
|
|
229
|
+
random: { type: "boolean", short: "r", default: false }, // from before scanning types: -o random
|
|
230
|
+
docker: { type: "boolean", short: "d", default: false },
|
|
231
|
+
worker: { type: "boolean", default: false }, // in a parallel scan's container
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
if (a.worker) {
|
|
235
|
+
await worker();
|
|
236
|
+
process.exit(); // its stdin may still be open: its app didn't log in
|
|
237
|
+
}
|
|
238
|
+
forgetLogin();
|
|
239
|
+
if (a.help)
|
|
240
|
+
return console.log(HELP);
|
|
241
|
+
const [subcommand, ...extra] = positionals;
|
|
242
|
+
if (subcommand !== undefined && (subcommand !== "connect" || extra.length))
|
|
243
|
+
throw new Error(`unknown command '${positionals.join(" ")}': see --help`);
|
|
244
|
+
if (subcommand === "connect" && Object.entries(a).some(([k, v]) => k !== "timeout" && v))
|
|
245
|
+
throw new Error("windscout connect takes only -t: see --help");
|
|
246
|
+
checkUpdate(); // runs while the app starts and the user picks, before the VPN starts switching
|
|
247
|
+
if (a.version) {
|
|
248
|
+
await checkUpdate();
|
|
249
|
+
console.log(versionLines().join("\n"));
|
|
250
|
+
if (canUpdate() && process.stdin.isTTY && out.isTTY && /^y(es)?$/i.test((await ask("update now? (y/n) ")) ?? ""))
|
|
251
|
+
process.exitCode = runUpdate();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const timeout = a.timeout ? Number(a.timeout) : undefined;
|
|
255
|
+
if (timeout !== undefined && !(timeout >= MIN_TIMEOUT))
|
|
256
|
+
throw new Error(`--timeout must be at least ${MIN_TIMEOUT} seconds`);
|
|
257
|
+
const hold = a.hold ? Number(a.hold) : undefined;
|
|
258
|
+
if (hold !== undefined && !(hold >= MIN_HOLD))
|
|
259
|
+
throw new Error(`--hold must be at least ${MIN_HOLD} seconds`);
|
|
260
|
+
if (a.order !== undefined && !ORDERS.includes(a.order))
|
|
261
|
+
throw new Error(`--order must be one of: ${ORDERS.join(", ")}`);
|
|
262
|
+
const tty = Boolean(process.stdin.isTTY && out.isTTY);
|
|
263
|
+
// a later scan: the picker, with what was saved
|
|
264
|
+
const again = { speedTest: true, firewall: false, stopAtFirst: false, allPorts: false, docker: false, all: a.all };
|
|
265
|
+
let flags = {
|
|
266
|
+
countries: a.countries ? list(a.countries) : undefined,
|
|
267
|
+
protocols: a.protocols ? list(a.protocols) : undefined,
|
|
268
|
+
timeout,
|
|
269
|
+
hold,
|
|
270
|
+
order: (a.order ?? (a.random ? "random" : undefined)),
|
|
271
|
+
speedTest: !a["no-speed"],
|
|
272
|
+
firewall: a.firewall,
|
|
273
|
+
stopAtFirst: a["stop-at-first"],
|
|
274
|
+
allPorts: a["all-ports"],
|
|
275
|
+
docker: a.docker,
|
|
276
|
+
all: a.all,
|
|
277
|
+
};
|
|
278
|
+
if (subcommand === "connect") {
|
|
279
|
+
const t = timeout ?? loadConfig()?.timeout ?? DEFAULT_TIMEOUT;
|
|
280
|
+
if (!tty)
|
|
281
|
+
return connectLast(t);
|
|
282
|
+
const last = loadLast();
|
|
283
|
+
if (!last) {
|
|
284
|
+
console.log(yellow("no saved scan yet: run windscout to scan first"));
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await waitForLogin();
|
|
289
|
+
// the best that still works, on the results screen, which keeps you connected
|
|
290
|
+
const to = await showResults({ entries: last.results, at: last.at, timeout: t, saved: true, connect: true, checker: () => dockerChecker(t) });
|
|
291
|
+
if (to === "quit")
|
|
292
|
+
return;
|
|
293
|
+
flags = again;
|
|
294
|
+
}
|
|
295
|
+
for (;;) {
|
|
296
|
+
const found = await scanOnce(flags, tty);
|
|
297
|
+
if (found === null)
|
|
298
|
+
return;
|
|
299
|
+
if (found === "update")
|
|
300
|
+
return updateAndRestart();
|
|
301
|
+
if (!tty)
|
|
302
|
+
return;
|
|
303
|
+
// nothing worked: the saved ones, which may work again; the screen still lets you scan again
|
|
304
|
+
const last = found.results.length ? null : loadLast();
|
|
305
|
+
const to = await showResults({
|
|
306
|
+
entries: last?.results ?? found.results,
|
|
307
|
+
at: last?.at ?? Date.now(),
|
|
308
|
+
timeout: found.timeout,
|
|
309
|
+
saved: !!last,
|
|
310
|
+
note: last ? "nothing connected in this scan: these are your saved connections, from earlier" : undefined,
|
|
311
|
+
checker: () => dockerChecker(found.timeout),
|
|
312
|
+
});
|
|
313
|
+
if (to === "quit")
|
|
314
|
+
return;
|
|
315
|
+
flags = again;
|
|
316
|
+
}
|
|
258
317
|
}
|
|
259
318
|
main().catch(async (e) => {
|
|
260
319
|
leaveScreen();
|
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";
|
|
@@ -8,9 +9,12 @@ import { COUNTRIES, PROTOCOLS } from "./windscribe.js";
|
|
|
8
9
|
export const SPEED_TEST = true;
|
|
9
10
|
export const DEFAULT_TIMEOUT = 15;
|
|
10
11
|
export const MIN_TIMEOUT = 10; // seconds; shorter cuts off connections that would have worked
|
|
12
|
+
export const DEFAULT_HOLD = 30;
|
|
13
|
+
export const MIN_HOLD = 10; // seconds; shorter says little about how long a connection lasts
|
|
11
14
|
export const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "windscout", "config.json");
|
|
12
15
|
const DOCKER_FILE = join(dirname(CONFIG), "docker.json"); // what the last parallel scan found its containers can do
|
|
13
16
|
export const LAST_FILE = join(dirname(CONFIG), "last.json"); // the last scan's working connections
|
|
17
|
+
const VIEW_FILE = join(dirname(CONFIG), "view.json"); // the table the scan screen was left on
|
|
14
18
|
/** The saved choice, minus anything this version doesn't offer; null on first run or an unreadable file */
|
|
15
19
|
export function loadConfig() {
|
|
16
20
|
try {
|
|
@@ -22,6 +26,7 @@ export function loadConfig() {
|
|
|
22
26
|
firewall: c.firewall === true,
|
|
23
27
|
order: ORDERS.includes(c.order) ? c.order : c.random === true ? "random" : "default", // before scanning types: random on or off
|
|
24
28
|
stopAtFirst: c.stopAtFirst === true,
|
|
29
|
+
hold: c.hold >= MIN_HOLD ? c.hold : DEFAULT_HOLD,
|
|
25
30
|
allPorts: c.allPorts === true,
|
|
26
31
|
timeout: c.timeout >= MIN_TIMEOUT ? c.timeout : DEFAULT_TIMEOUT,
|
|
27
32
|
docker: c.docker === true,
|
|
@@ -31,13 +36,30 @@ export function loadConfig() {
|
|
|
31
36
|
return null;
|
|
32
37
|
}
|
|
33
38
|
}
|
|
39
|
+
/** Saves these settings over the saved ones: the rest of the file stays (the results screen's, the picker's) */
|
|
34
40
|
export function saveConfig(c) {
|
|
41
|
+
let saved = {};
|
|
42
|
+
try {
|
|
43
|
+
saved = JSON.parse(readFileSync(CONFIG, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch { } // first run, or unreadable: start over
|
|
35
46
|
try {
|
|
36
47
|
mkdirSync(dirname(CONFIG), { recursive: true });
|
|
37
|
-
writeFileSync(CONFIG, JSON.stringify(c, null, 2) + "\n");
|
|
48
|
+
writeFileSync(CONFIG, JSON.stringify({ ...saved, ...c }, null, 2) + "\n");
|
|
38
49
|
}
|
|
39
50
|
catch { } // not being able to remember isn't worth failing the scan
|
|
40
51
|
}
|
|
52
|
+
/** The results screen's saved settings: watching on until turned off; background checks as docker is for parallel scans until
|
|
53
|
+
* turned on or off on that screen */
|
|
54
|
+
export function loadSettings() {
|
|
55
|
+
try {
|
|
56
|
+
const c = JSON.parse(readFileSync(CONFIG, "utf8"));
|
|
57
|
+
return { watch: c.watch !== false, background: typeof c.background === "boolean" ? c.background : c.docker === true };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { watch: true, background: false };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
41
63
|
/** Whether WireGuard worked in the docker containers last time (yes until a scan finds otherwise), for the picker's estimate */
|
|
42
64
|
export function loadDockerWireGuard() {
|
|
43
65
|
try {
|
|
@@ -54,6 +76,22 @@ export function saveDockerWireGuard(wireguard) {
|
|
|
54
76
|
}
|
|
55
77
|
catch { } // not being able to remember isn't worth failing the scan
|
|
56
78
|
}
|
|
79
|
+
/** The table the last scan was left on; the ranking until the user switches */
|
|
80
|
+
export function loadView() {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(readFileSync(VIEW_FILE, "utf8")).view === "availability" ? "availability" : "ranking";
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return "ranking";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function saveView(view) {
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(dirname(VIEW_FILE), { recursive: true });
|
|
91
|
+
writeFileSync(VIEW_FILE, JSON.stringify({ view }) + "\n");
|
|
92
|
+
}
|
|
93
|
+
catch { } // not being able to remember isn't worth failing the scan
|
|
94
|
+
}
|
|
57
95
|
/** The last scan that found something; null when there's none or the file is unreadable */
|
|
58
96
|
export function loadLast() {
|
|
59
97
|
try {
|
|
@@ -66,12 +104,13 @@ export function loadLast() {
|
|
|
66
104
|
}
|
|
67
105
|
}
|
|
68
106
|
/** Saves a scan's working connections; a scan where nothing worked keeps the last one that found something (the network may
|
|
69
|
-
* just be down). With tried (the "query protocol" pairs the scan tried), the saved connections it didn't try stay, after its own
|
|
107
|
+
* just be down). With tried (the "query protocol" pairs the scan tried), the saved connections it didn't try stay, after its own
|
|
108
|
+
* working ones and before the ones whose last check failed */
|
|
70
109
|
export function saveLast(results, at = Date.now(), tried) {
|
|
71
110
|
if (!results.length)
|
|
72
111
|
return;
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
const all = [...results, ...(tried ? (loadLast()?.results ?? []).filter((r) => !tried.has(`${r.query} ${r.protocol}`)) : [])];
|
|
113
|
+
results = [...all.filter((r) => !r.failed), ...all.filter((r) => r.failed)];
|
|
75
114
|
try {
|
|
76
115
|
mkdirSync(dirname(LAST_FILE), { recursive: true });
|
|
77
116
|
writeFileSync(LAST_FILE, JSON.stringify({ at, results }, null, 2) + "\n");
|