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/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 { CONFIG, DEFAULT_TIMEOUT, forgetLogin, LAST_FILE, loadConfig, loadDockerWireGuard, loadLast, MIN_TIMEOUT, saveDockerWireGuard, saveLast, SPEED_TEST } from "./config.js";
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 { attempt, disconnect, MIN_MBPS, probe } from "./probe.js";
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 { command, report, target } from "./tui/report.js";
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 { cyan, green, yellow } from "./tui/text.js";
18
- import { duration, now } from "./time.js";
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, windscribe, withPorts } from "./windscribe.js";
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, then about 10 more seconds connected, with internet through it
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
- const ask = (question) => new Promise((resolve) => {
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
- for (const [i, r] of last.results.entries()) {
97
- console.log(`\n${i + 1}/${last.results.length} ${r.location || r.query} over ${r.protocol}`);
98
- if (await connectTo(r, timeout))
99
- return;
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
- async function main() {
116
- const { values: a, positionals } = parseArgs({
117
- allowPositionals: true,
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
- const tty = Boolean(process.stdin.isTTY && out.isTTY);
164
- let countries = a.countries ? list(a.countries) : undefined;
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 || !a.countries || fromLast ? await protocolPorts() : undefined;
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
- let probers = [probe], after; // after: what runs here once the containers are done
222
- const later = notInContainers(scanned, loadDockerWireGuard()); // as far as the last parallel scan knows
223
- const inContainers = attempts.filter((x) => !later.has(x.protocol)).length;
224
- if (docker && inContainers) {
225
- const started = await startParallel(inContainers);
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
- after = { prober: probe, protocols: notInContainers(scanned, started.wireguard), name: "this computer" };
231
- await windscribe("disconnect"); // a VPN connection here would carry the containers' traffic too
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: attempts, stopAtFirst }, probers, after);
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 && !after && (await blockInternet());
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 (after)
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("stopped at the first connection that held up (a speed test, then staying up)"));
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
- const choices = report(scan.results, scanned, { all: a.all, speedTest: speedTest || stopAtFirst }); // stop at the first measures speed
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(choices, undefined, tried);
207
+ saveLast(results, undefined, tried);
253
208
  await checkUpdate();
254
209
  if (updateNotice())
255
210
  console.error("\n" + versionLines().join("\n"));
256
- if (tty && choices.length)
257
- await offerConnect(choices, timeout);
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, and the last scan's working results, remembered between runs.
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
- if (tried)
74
- results = [...results, ...(loadLast()?.results ?? []).filter((r) => !tried.has(`${r.query} ${r.protocol}`))];
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");