windscout 0.7.1 → 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
@@ -59,36 +72,8 @@ const ask = (question, instant = []) => new Promise((resolve) => {
59
72
  rl.on("close", () => (process.stdin.off("keypress", onKey), resolve(null)));
60
73
  rl.question(question, (a) => (resolve(a.trim()), rl.close()));
61
74
  });
62
- /** Runs a result's connect command and checks it as the scan does: connected, on its port, with internet through it.
63
- * True once it is; otherwise disconnects (and waits until the app has), so the next connect isn't swallowed */
64
- async function connectTo(r, timeout) {
65
- console.log(cyan(command(r)));
66
- const t = now(), { status, location } = await attempt(target(r), r.protocol, { timeout, speedTest: false });
67
- if (status === "ok")
68
- return console.log(green(`Connected: ${location}`)), true;
69
- console.log(yellow(`${status} after ${duration(now() - t)}`));
70
- await disconnect();
71
- return false;
72
- }
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 */
75
- async function offerConnect(choices, timeout) {
76
- const others = choices.length > 2 ? `, 1-${choices.length - 1} = other` : choices.length > 1 ? ", 1 = other" : "";
77
- for (;;) {
78
- const a = await ask(`\nconnect to # (enter or 0 = best${others}, q = quit): `, ["0", "q"]);
79
- if (a === null || /^q(uit)?$/i.test(a))
80
- return;
81
- const r = a === "" ? choices[0] : /^\d+$/.test(a) ? choices[Number(a)] : undefined;
82
- if (!r) {
83
- console.log(yellow(`no #${a} in the tables`));
84
- continue;
85
- }
86
- await connectTo(r, timeout);
87
- return;
88
- }
89
- }
90
75
  const RESCAN_HINT = "rescan them with windscout -o last-only, or start a new scan";
91
- /** 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 */
92
77
  async function connectLast(timeout) {
93
78
  const last = loadLast();
94
79
  if (!last) {
@@ -98,13 +83,11 @@ async function connectLast(timeout) {
98
83
  }
99
84
  await waitForLogin();
100
85
  await disconnect(); // a connection already up would read as the first one connecting
101
- for (const [i, r] of last.results.entries()) {
102
- console.log(`\n${i + 1}/${last.results.length} ${r.location || r.query} over ${r.protocol}`);
103
- if (await connectTo(r, timeout))
104
- 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;
105
90
  }
106
- console.log(yellow(`\nnone of the last result's ${last.results.length} connection${last.results.length === 1 ? "" : "s"} works now: ${RESCAN_HINT}`));
107
- process.exitCode = 1;
108
91
  }
109
92
  /** u in the picker: update, then run the new version in this one's place, with the same arguments */
110
93
  function updateAndRestart() {
@@ -117,63 +100,12 @@ function updateAndRestart() {
117
100
  console.log(green("updated · restarting windscout"));
118
101
  process.exitCode = spawnSync(process.execPath, [...process.execArgv, ...process.argv.slice(1)], { stdio: "inherit" }).status ?? 1;
119
102
  }
120
- async function main() {
121
- const { values: a, positionals } = parseArgs({
122
- allowPositionals: true,
123
- options: {
124
- countries: { type: "string", short: "c" },
125
- protocols: { type: "string", short: "p" },
126
- "all-ports": { type: "boolean", short: "P", default: false },
127
- timeout: { type: "string", short: "t" },
128
- all: { type: "boolean", short: "a", default: false },
129
- help: { type: "boolean", short: "h", default: false },
130
- version: { type: "boolean", short: "v", default: false },
131
- "no-speed": { type: "boolean", short: "n", default: false },
132
- firewall: { type: "boolean", short: "f", default: false },
133
- order: { type: "string", short: "o" },
134
- "stop-at-first": { type: "boolean", short: "w", default: false },
135
- random: { type: "boolean", short: "r", default: false }, // from before scanning types: -o random
136
- docker: { type: "boolean", short: "d", default: false },
137
- worker: { type: "boolean", default: false }, // in a parallel scan's container
138
- },
139
- });
140
- if (a.worker) {
141
- await worker();
142
- process.exit(); // its stdin may still be open: its app didn't log in
143
- }
144
- forgetLogin();
145
- if (a.help)
146
- return console.log(HELP);
147
- const [subcommand, ...extra] = positionals;
148
- if (subcommand !== undefined && (subcommand !== "connect" || extra.length))
149
- throw new Error(`unknown command '${positionals.join(" ")}': see --help`);
150
- if (subcommand === "connect" && Object.entries(a).some(([k, v]) => k !== "timeout" && v))
151
- throw new Error("windscout connect takes only -t: see --help");
152
- checkUpdate(); // runs while the app starts and the user picks, before the VPN starts switching
153
- if (a.version) {
154
- await checkUpdate();
155
- console.log(versionLines().join("\n"));
156
- if (canUpdate() && process.stdin.isTTY && out.isTTY && /^y(es)?$/i.test((await ask("update now? (y/n) ")) ?? ""))
157
- process.exitCode = runUpdate();
158
- return;
159
- }
160
- let timeout = a.timeout ? Number(a.timeout) : undefined;
161
- if (timeout !== undefined && !(timeout >= MIN_TIMEOUT))
162
- throw new Error(`--timeout must be at least ${MIN_TIMEOUT} seconds`);
163
- if (a.order !== undefined && !ORDERS.includes(a.order))
164
- throw new Error(`--order must be one of: ${ORDERS.join(", ")}`);
165
- if (subcommand === "connect")
166
- 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) {
167
106
  const status = await waitForLogin();
168
- const tty = Boolean(process.stdin.isTTY && out.isTTY);
169
- let countries = a.countries ? list(a.countries) : undefined;
170
- let protocols = a.protocols ? list(a.protocols) : undefined;
171
- let speedTest = SPEED_TEST && !a["no-speed"];
172
- let firewall = a.firewall;
173
- let order = (a.order ?? (a.random ? "random" : undefined));
174
- let stopAtFirst = a["stop-at-first"];
175
- let allPorts = a["all-ports"];
176
- let docker = a.docker;
107
+ let { countries, protocols, speedTest, firewall, order, stopAtFirst, allPorts, docker, timeout, hold } = f;
108
+ speedTest &&= SPEED_TEST;
177
109
  const last = loadLast();
178
110
  // only the last results needs no countries: without a terminal, it goes without the picker
179
111
  if (!countries && !(order === "last-only" && !tty)) {
@@ -189,20 +121,20 @@ async function main() {
189
121
  firewall: firewall || isFirewallOn(status) || (saved?.firewall ?? false),
190
122
  order: order ?? saved?.order ?? "default",
191
123
  stopAtFirst: stopAtFirst || (saved?.stopAtFirst ?? false),
124
+ hold: hold ?? saved?.hold ?? DEFAULT_HOLD,
192
125
  allPorts: allPorts || (saved?.allPorts ?? false),
193
126
  docker: docker || (saved?.docker ?? false),
194
127
  last,
195
128
  });
196
- if (!choice)
197
- return;
198
- if (choice === "update")
199
- return updateAndRestart();
129
+ if (!choice || choice === "update")
130
+ return choice;
200
131
  countries = choice.countries;
201
132
  protocols ??= choice.protocols;
202
133
  speedTest = choice.speed;
203
134
  firewall = choice.firewall;
204
135
  order = choice.order;
205
136
  stopAtFirst = choice.stopAtFirst;
137
+ hold = choice.hold;
206
138
  allPorts = choice.allPorts;
207
139
  timeout = choice.timeout;
208
140
  docker = choice.docker;
@@ -210,6 +142,7 @@ async function main() {
210
142
  countries ??= [];
211
143
  protocols ??= PROTOCOLS;
212
144
  timeout ??= DEFAULT_TIMEOUT;
145
+ hold ??= DEFAULT_HOLD;
213
146
  order ??= "default";
214
147
  const byLast = order === "last-first" || order === "last-only"; // a type by the last result: it needs a saved scan
215
148
  if (byLast && !last)
@@ -217,7 +150,7 @@ async function main() {
217
150
  const fromLast = byLast ? last : null; // the saved connections this scan uses
218
151
  // the picker asked for the ports already: waiting for that keeps windscribe-cli free for the scan.
219
152
  // The saved connections need them too, to tell a saved "tcp:443" from tcp on its default port
220
- const lists = allPorts || !a.countries || fromLast ? await protocolPorts() : undefined;
153
+ const lists = allPorts || !f.countries || fromLast ? await protocolPorts() : undefined;
221
154
  if (allPorts && lists)
222
155
  protocols = withPorts(protocols, lists);
223
156
  const attempts = plan(order, countries, protocols, fromLast?.results, lists);
@@ -234,7 +167,7 @@ async function main() {
234
167
  throw nothingLeft();
235
168
  const started = await startParallel(inContainers().length);
236
169
  if (!started)
237
- return leaveScreen();
170
+ return leaveScreen(), null;
238
171
  saveDockerWireGuard(started.wireguard);
239
172
  probers = started.probers;
240
173
  skipped = notInContainers(scanned, started.wireguard); // their kernel may not run WireGuard after all, or may now
@@ -242,9 +175,12 @@ async function main() {
242
175
  await stopWorkers();
243
176
  throw nothingLeft();
244
177
  }
245
- await windscribe("disconnect"); // a VPN connection here would carry the containers' traffic too
246
178
  }
247
- const scan = new Scan(countries, scanned.filter((p) => !skipped.has(p)), { timeout, speedTest, only: inContainers(), stopAtFirst }, probers);
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();
248
184
  // not with containers: their traffic goes out through this computer, and the firewall would block it
249
185
  const blocking = firewall && !docker && (await blockInternet());
250
186
  const view = tty ? showProgress(scan, blocking) : logProgress(scan);
@@ -258,19 +194,126 @@ async function main() {
258
194
  if (skipped.size)
259
195
  console.error(yellow(`not tried: ${[...skipped].join(", ")}, which the docker containers can't run (a scan without --docker can)`));
260
196
  if (scan.found)
261
- 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`));
262
198
  else if (scan.stopping)
263
199
  console.log(yellow("interrupted, showing partial results"));
264
- const choices = report(scan.results, scan.protocols, { 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
265
204
  // its working connections replace the saved ones it tried, so what stopped working drops out; the saved ones it didn't try
266
205
  // (other countries, or what a stopped scan didn't get to) stay, after its own; a scan where nothing worked keeps the last
267
206
  const tried = new Set(scan.results.filter((r) => r.status !== "interrupted").map((r) => `${r.query} ${r.protocol}`));
268
- saveLast(choices, undefined, tried);
207
+ saveLast(results, undefined, tried);
269
208
  await checkUpdate();
270
209
  if (updateNotice())
271
210
  console.error("\n" + versionLines().join("\n"));
272
- if (tty && choices.length)
273
- 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
+ }
274
317
  }
275
318
  main().catch(async (e) => {
276
319
  leaveScreen();
package/dist/config.js CHANGED
@@ -9,6 +9,8 @@ import { COUNTRIES, PROTOCOLS } from "./windscribe.js";
9
9
  export const SPEED_TEST = true;
10
10
  export const DEFAULT_TIMEOUT = 15;
11
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
12
14
  export const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "windscout", "config.json");
13
15
  const DOCKER_FILE = join(dirname(CONFIG), "docker.json"); // what the last parallel scan found its containers can do
14
16
  export const LAST_FILE = join(dirname(CONFIG), "last.json"); // the last scan's working connections
@@ -24,6 +26,7 @@ export function loadConfig() {
24
26
  firewall: c.firewall === true,
25
27
  order: ORDERS.includes(c.order) ? c.order : c.random === true ? "random" : "default", // before scanning types: random on or off
26
28
  stopAtFirst: c.stopAtFirst === true,
29
+ hold: c.hold >= MIN_HOLD ? c.hold : DEFAULT_HOLD,
27
30
  allPorts: c.allPorts === true,
28
31
  timeout: c.timeout >= MIN_TIMEOUT ? c.timeout : DEFAULT_TIMEOUT,
29
32
  docker: c.docker === true,
@@ -33,13 +36,30 @@ export function loadConfig() {
33
36
  return null;
34
37
  }
35
38
  }
39
+ /** Saves these settings over the saved ones: the rest of the file stays (the results screen's, the picker's) */
36
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
37
46
  try {
38
47
  mkdirSync(dirname(CONFIG), { recursive: true });
39
- writeFileSync(CONFIG, JSON.stringify(c, null, 2) + "\n");
48
+ writeFileSync(CONFIG, JSON.stringify({ ...saved, ...c }, null, 2) + "\n");
40
49
  }
41
50
  catch { } // not being able to remember isn't worth failing the scan
42
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
+ }
43
63
  /** Whether WireGuard worked in the docker containers last time (yes until a scan finds otherwise), for the picker's estimate */
44
64
  export function loadDockerWireGuard() {
45
65
  try {
@@ -84,12 +104,13 @@ export function loadLast() {
84
104
  }
85
105
  }
86
106
  /** Saves a scan's working connections; a scan where nothing worked keeps the last one that found something (the network may
87
- * 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 */
88
109
  export function saveLast(results, at = Date.now(), tried) {
89
110
  if (!results.length)
90
111
  return;
91
- if (tried)
92
- 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)];
93
114
  try {
94
115
  mkdirSync(dirname(LAST_FILE), { recursive: true });
95
116
  writeFileSync(LAST_FILE, JSON.stringify({ at, results }, null, 2) + "\n");