9remote 0.1.63 → 2.0.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 +179 -62
- package/cli/index.js +704 -294
- package/cli/utils/assets/trayIcon.ico +0 -0
- package/cli/utils/cloudflared.js +72 -36
- package/cli/utils/permissions.js +5 -5
- package/cli/utils/pids.js +114 -0
- package/cli/utils/token.js +3 -1
- package/cli/utils/tray.js +251 -0
- package/cli/utils/tui.js +209 -34
- package/cli/utils/updateChecker.js +107 -13
- package/dist/assets/trayIcon.ico +0 -0
- package/dist/cli.cjs +1 -82
- package/dist/ptyDaemon.cjs +1 -10
- package/dist/server.cjs +2 -184
- package/dist/ui/assets/{index-BfTPkO8b.css → index-COWVKicT.css} +1 -1
- package/dist/ui/assets/index-njTKNAa6.js +8 -0
- package/dist/ui/index.html +2 -2
- package/index.js +174 -618
- package/lib/constants.js +64 -0
- package/lib/deviceApproval.js +116 -0
- package/lib/router.js +134 -0
- package/lib/socketio.js +168 -25
- package/package.json +6 -1
- package/dist/ui/assets/index-B37vtDoz.js +0 -8
package/cli/utils/tui.js
CHANGED
|
@@ -26,7 +26,7 @@ const W = () => Math.min(44, process.stdout.columns || 44);
|
|
|
26
26
|
|
|
27
27
|
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
28
28
|
|
|
29
|
-
export function
|
|
29
|
+
export function getBannerText(currentVersion, latestVersion = null) {
|
|
30
30
|
const w = W();
|
|
31
31
|
const inner = w - 2;
|
|
32
32
|
|
|
@@ -43,58 +43,161 @@ export function showBanner(currentVersion, latestVersion = null) {
|
|
|
43
43
|
return C.orange + "║" + C.reset + " ".repeat(lp) + colorFn(text) + " ".repeat(rp) + C.orange + "║" + C.reset;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
const lines = [
|
|
47
|
+
"",
|
|
48
|
+
C.orange + "╔" + "═".repeat(inner) + "╗" + C.reset,
|
|
49
|
+
line(),
|
|
50
|
+
center(`🚀 9Remote v${currentVersion}`, (s) => C.bold + C.orange + s + C.reset),
|
|
51
|
+
center("Remote terminal access from anywhere", (s) => C.dim + s + C.reset),
|
|
52
|
+
line(),
|
|
53
|
+
];
|
|
52
54
|
|
|
53
55
|
if (latestVersion) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.log(center(hint, (s) => C.dim + s + C.reset));
|
|
58
|
-
console.log(line());
|
|
56
|
+
lines.push(center(`⬆ New version v${latestVersion} available!`, (s) => C.yellow + C.bold + s + C.reset));
|
|
57
|
+
lines.push(center(`Run: npm i -g 9remote@latest`, (s) => C.dim + s + C.reset));
|
|
58
|
+
lines.push(line());
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
lines.push(C.orange + "╚" + "═".repeat(inner) + "╝" + C.reset);
|
|
62
|
+
lines.push("");
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function showBanner(currentVersion, latestVersion = null) {
|
|
67
|
+
console.log(getBannerText(currentVersion, latestVersion));
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
// ── Progress ──────────────────────────────────────────────────────────────────
|
|
66
71
|
|
|
72
|
+
const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
73
|
+
|
|
67
74
|
const STEPS = [
|
|
68
|
-
{ label: "Preparing",
|
|
69
|
-
{ label: "Connecting",
|
|
70
|
-
{ label: "Starting tunnel",
|
|
71
|
-
{ label: "
|
|
75
|
+
{ label: "Preparing", desc: "Checking dependencies" },
|
|
76
|
+
{ label: "Connecting", desc: "Creating session" },
|
|
77
|
+
{ label: "Starting tunnel", desc: "Spawning tunnel" },
|
|
78
|
+
{ label: "Verifying tunnel", desc: "Health check" },
|
|
79
|
+
{ label: "Ready", desc: "Tunnel is live" },
|
|
72
80
|
];
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
const IS_WIN = process.platform === "win32";
|
|
83
|
+
const SPINNER_INTERVAL_MS = IS_WIN ? 120 : 80;
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
// Ensure cursor is restored on any unexpected exit
|
|
86
|
+
process.on("exit", () => process.stdout.write("\x1b[?25h"));
|
|
87
|
+
process.on("SIGINT", () => { process.stdout.write("\x1b[?25h"); });
|
|
88
|
+
process.on("SIGTERM", () => { process.stdout.write("\x1b[?25h"); });
|
|
80
89
|
|
|
90
|
+
let _progressLines = 0;
|
|
91
|
+
let _spinnerInterval = null;
|
|
92
|
+
let _spinnerFrame = 0;
|
|
93
|
+
let _activeIdx = -1;
|
|
94
|
+
let _activeDesc = null;
|
|
95
|
+
let _infoLine = null;
|
|
96
|
+
let _cursorHidden = false;
|
|
97
|
+
|
|
98
|
+
function _buildLines() {
|
|
81
99
|
const lines = [];
|
|
100
|
+
const isFinal = _activeIdx === STEPS.length - 1;
|
|
82
101
|
STEPS.forEach((step, i) => {
|
|
83
|
-
|
|
102
|
+
const desc = (i === _activeIdx && _activeDesc) ? _activeDesc : step.desc;
|
|
103
|
+
if (i < _activeIdx || (isFinal && i === _activeIdx)) {
|
|
84
104
|
lines.push(` ${C.green}✓${C.reset} ${C.dim}${step.label}${C.reset}`);
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
if (_infoLine && i === _infoLine.afterIdx) {
|
|
106
|
+
lines.push(` ${C.green}✓${C.reset} ${C.cyan}${_infoLine.text}${C.reset}`);
|
|
107
|
+
}
|
|
108
|
+
} else if (i === _activeIdx) {
|
|
109
|
+
const frame = SPINNER_FRAMES[_spinnerFrame % SPINNER_FRAMES.length];
|
|
110
|
+
lines.push(` ${C.orange}${frame}${C.reset} ${C.bold}${step.label}${C.reset} ${C.dim}${desc}${C.reset}`);
|
|
87
111
|
} else {
|
|
88
112
|
lines.push(` ${C.dim}○ ${step.label}${C.reset}`);
|
|
89
113
|
}
|
|
90
114
|
});
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
91
117
|
|
|
92
|
-
|
|
118
|
+
function _fullRedraw() {
|
|
119
|
+
const lines = _buildLines();
|
|
120
|
+
if (_progressLines > 0) {
|
|
121
|
+
process.stdout.write(`\x1b[${_progressLines}A\x1b[0J`);
|
|
122
|
+
}
|
|
123
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
93
124
|
_progressLines = lines.length;
|
|
94
125
|
}
|
|
95
126
|
|
|
127
|
+
// Only repaint the active spinner line to avoid flicker on Windows conhost
|
|
128
|
+
function _tickSpinner() {
|
|
129
|
+
if (_progressLines === 0 || _activeIdx < 0) return;
|
|
130
|
+
const lines = _buildLines();
|
|
131
|
+
if (lines.length !== _progressLines) {
|
|
132
|
+
_fullRedraw();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const activeLineOffset = _activeIdx + (_infoLine && _infoLine.afterIdx < _activeIdx ? 1 : 0);
|
|
136
|
+
const up = _progressLines - activeLineOffset;
|
|
137
|
+
// Move up, clear line, write, move back down — single write = no flicker
|
|
138
|
+
process.stdout.write(`\x1b[${up}A\r\x1b[2K${lines[activeLineOffset]}\x1b[${up}B\r`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _hideCursor() {
|
|
142
|
+
if (!_cursorHidden) {
|
|
143
|
+
process.stdout.write("\x1b[?25l");
|
|
144
|
+
_cursorHidden = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _showCursor() {
|
|
149
|
+
if (_cursorHidden) {
|
|
150
|
+
process.stdout.write("\x1b[?25h");
|
|
151
|
+
_cursorHidden = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function renderProgress(activeIdx, redraw = false, desc = null) {
|
|
156
|
+
if (_spinnerInterval) {
|
|
157
|
+
clearInterval(_spinnerInterval);
|
|
158
|
+
_spinnerInterval = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_activeIdx = activeIdx;
|
|
162
|
+
_activeDesc = desc;
|
|
163
|
+
_spinnerFrame = 0;
|
|
164
|
+
|
|
165
|
+
if (!redraw) _progressLines = 0;
|
|
166
|
+
_fullRedraw();
|
|
167
|
+
|
|
168
|
+
if (activeIdx < STEPS.length - 1) {
|
|
169
|
+
_hideCursor();
|
|
170
|
+
_spinnerInterval = setInterval(() => {
|
|
171
|
+
_spinnerFrame++;
|
|
172
|
+
_tickSpinner();
|
|
173
|
+
}, SPINNER_INTERVAL_MS);
|
|
174
|
+
} else {
|
|
175
|
+
_showCursor();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Show an extra info line after a completed step */
|
|
180
|
+
export function setProgressInfo(afterIdx, text) {
|
|
181
|
+
_infoLine = text ? { afterIdx, text } : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Update desc of current active step without changing step index */
|
|
185
|
+
export function updateProgressDesc(desc) {
|
|
186
|
+
_activeDesc = desc;
|
|
187
|
+
if (_progressLines > 0) _tickSpinner();
|
|
188
|
+
}
|
|
189
|
+
|
|
96
190
|
export function resetProgress() {
|
|
191
|
+
if (_spinnerInterval) {
|
|
192
|
+
clearInterval(_spinnerInterval);
|
|
193
|
+
_spinnerInterval = null;
|
|
194
|
+
}
|
|
195
|
+
_showCursor();
|
|
97
196
|
_progressLines = 0;
|
|
197
|
+
_activeIdx = -1;
|
|
198
|
+
_activeDesc = null;
|
|
199
|
+
_infoLine = null;
|
|
200
|
+
_spinnerFrame = 0;
|
|
98
201
|
}
|
|
99
202
|
|
|
100
203
|
// ── selectMenu ────────────────────────────────────────────────────────────────
|
|
@@ -126,7 +229,7 @@ export function selectMenu(title, items, defaultIndex = 0, headerContent = "", o
|
|
|
126
229
|
if (header) {
|
|
127
230
|
process.stdout.write(header + "\n");
|
|
128
231
|
}
|
|
129
|
-
process.stdout.write(`${C.dim}${title}${C.reset}\n\n`);
|
|
232
|
+
if (title) process.stdout.write(`${C.dim}${title}${C.reset}\n\n`);
|
|
130
233
|
items.forEach((item, i) => {
|
|
131
234
|
const icon = i === selected ? (isWin ? ">" : "★") : (isWin ? " " : "☆");
|
|
132
235
|
if (i === selected) {
|
|
@@ -191,15 +294,87 @@ export function selectMenu(title, items, defaultIndex = 0, headerContent = "", o
|
|
|
191
294
|
*/
|
|
192
295
|
export function confirm(message) {
|
|
193
296
|
return new Promise((resolve) => {
|
|
194
|
-
//
|
|
297
|
+
// Clean state
|
|
298
|
+
process.stdin.removeAllListeners("keypress");
|
|
195
299
|
if (process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch {} }
|
|
300
|
+
process.stdin.pause();
|
|
301
|
+
|
|
302
|
+
process.stdout.write(`${message} (y/N): `);
|
|
303
|
+
|
|
304
|
+
// Use raw keypress (same pattern as selectMenu)
|
|
305
|
+
readline.emitKeypressEvents(process.stdin);
|
|
306
|
+
if (process.stdin.isTTY) { try { process.stdin.setRawMode(true); } catch {} }
|
|
307
|
+
process.stdin.resume();
|
|
308
|
+
|
|
309
|
+
const onKeypress = (str, key) => {
|
|
310
|
+
if (!key) return;
|
|
311
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
312
|
+
if (process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch {} }
|
|
313
|
+
process.stdin.pause();
|
|
314
|
+
|
|
315
|
+
if (key.ctrl && key.name === "c") { process.stdout.write("\n"); process.exit(0); }
|
|
316
|
+
const approved = (key.name || "").toLowerCase() === "y";
|
|
317
|
+
console.log(approved ? "y" : "n");
|
|
318
|
+
resolve(approved);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
process.stdin.on("keypress", onKeypress);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Device Approval Prompt ────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Show device approval prompt with raw-mode single-key capture.
|
|
329
|
+
* Fully takes over stdin from selectMenu, resolves with true/false.
|
|
330
|
+
*/
|
|
331
|
+
export function showDeviceApproval(deviceId, ip) {
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
const shortId = deviceId ? deviceId.slice(0, 8) : "unknown";
|
|
334
|
+
const w = W();
|
|
335
|
+
|
|
336
|
+
// Fully take over stdin from selectMenu
|
|
196
337
|
process.stdin.removeAllListeners("keypress");
|
|
338
|
+
if (process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch {} }
|
|
339
|
+
process.stdin.pause();
|
|
340
|
+
|
|
341
|
+
// Clear screen for clean approval UI
|
|
342
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
343
|
+
console.log("");
|
|
344
|
+
console.log(`${C.orange}${'═'.repeat(w)}${C.reset}`);
|
|
345
|
+
console.log(`${C.orange}${C.bold} 🔔 New Device Connection${C.reset}`);
|
|
346
|
+
console.log(`${C.orange}${'═'.repeat(w)}${C.reset}`);
|
|
347
|
+
console.log(` Device: ${C.cyan}${shortId}...${C.reset}`);
|
|
348
|
+
console.log(` IP: ${C.cyan}${ip}${C.reset}`);
|
|
349
|
+
console.log(`${C.orange}${'═'.repeat(w)}${C.reset}`);
|
|
350
|
+
console.log("");
|
|
197
351
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
352
|
+
process.stdout.write(` Allow this device? ${C.dim}(y/n)${C.reset} `);
|
|
353
|
+
|
|
354
|
+
// Use keypress events (same pattern as selectMenu)
|
|
355
|
+
readline.emitKeypressEvents(process.stdin);
|
|
356
|
+
if (process.stdin.isTTY) { try { process.stdin.setRawMode(true); } catch {} }
|
|
357
|
+
process.stdin.resume();
|
|
358
|
+
|
|
359
|
+
const onKeypress = (str, key) => {
|
|
360
|
+
if (!key) return;
|
|
361
|
+
const ch = (key.name || "").toLowerCase();
|
|
362
|
+
if (ch === "y" || ch === "n" || key.name === "return" || (key.ctrl && key.name === "c")) {
|
|
363
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
364
|
+
if (process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch {} }
|
|
365
|
+
process.stdin.pause();
|
|
366
|
+
if (key.ctrl && key.name === "c") process.exit(0);
|
|
367
|
+
|
|
368
|
+
const approved = ch === "y";
|
|
369
|
+
console.log(approved ? `${C.green}y${C.reset}` : `${C.red}n${C.reset}`);
|
|
370
|
+
console.log(approved
|
|
371
|
+
? `\n ${C.green}\u2713 Device approved${C.reset}`
|
|
372
|
+
: `\n ${C.red}\u2717 Device rejected${C.reset}`);
|
|
373
|
+
setTimeout(() => resolve(approved), 500);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
process.stdin.on("keypress", onKeypress);
|
|
203
378
|
});
|
|
204
379
|
}
|
|
205
380
|
|
|
@@ -1,15 +1,63 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { spawn } from "child_process";
|
|
4
|
+
import { spawn, execSync } from "child_process";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import os from "os";
|
|
7
|
+
import { browserFetch } from "../../lib/constants.js";
|
|
8
|
+
import { killAll as killAllPids, getPidsDir } from "./pids.js";
|
|
7
9
|
|
|
8
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const PACKAGE_NAME = "9remote";
|
|
10
12
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
11
13
|
const UPDATE_CHECK_TIMEOUT = 3000;
|
|
12
14
|
const SAFETY_TIMEOUT = 8000;
|
|
15
|
+
const SERVER_PORT = 2208;
|
|
16
|
+
// Legacy path from pre-pids.js installs — clean up once so old instances
|
|
17
|
+
// can still be killed during upgrade from older versions.
|
|
18
|
+
const LEGACY_CLOUDFLARED_PID_FILE = path.join(os.homedir(), ".9remote", "cloudflared.pid");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Kill all 9remote-related child processes to release file locks before npm install.
|
|
22
|
+
*
|
|
23
|
+
* Critical on Windows: node.exe / tray helper / cloudflared.exe lock files inside
|
|
24
|
+
* node_modules\9remote\dist, so `npm i -g` fails with EBUSY when trying to rename
|
|
25
|
+
* the old dist folder.
|
|
26
|
+
*
|
|
27
|
+
* We kill ONLY by PID (from ~/.9remote/pids/) — never by image name (taskkill /IM)
|
|
28
|
+
* or by commandline match, because those would also kill unrelated apps on the
|
|
29
|
+
* machine that happen to use cloudflared.exe, tray_windows_release.exe, or node.exe.
|
|
30
|
+
*/
|
|
31
|
+
function cleanupBeforeUpdate() {
|
|
32
|
+
// 1. Kill tracked processes (cloudflared + agent tree) via pids.js.
|
|
33
|
+
// Agent kill with taskkill /F /T sweeps its server child + tray helper.
|
|
34
|
+
try { killAllPids(); } catch {}
|
|
35
|
+
|
|
36
|
+
// 2. Legacy: older versions stored cloudflared PID at a different path.
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(LEGACY_CLOUDFLARED_PID_FILE)) {
|
|
39
|
+
const pid = parseInt(readFileSync(LEGACY_CLOUDFLARED_PID_FILE, "utf8"));
|
|
40
|
+
if (Number.isFinite(pid)) { try { process.kill(pid); } catch {} }
|
|
41
|
+
try { unlinkSync(LEGACY_CLOUDFLARED_PID_FILE); } catch {}
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
// 3. Safety net for stale server on SERVER_PORT (e.g. crashed without clearing PID).
|
|
46
|
+
// Scoped to one specific port, so we only touch 9remote's own server.
|
|
47
|
+
try {
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
execSync(`for /f "tokens=5" %a in ('netstat -aon ^| findstr :${SERVER_PORT}') do taskkill /F /PID %a`, { stdio: "ignore", windowsHide: true });
|
|
50
|
+
} else {
|
|
51
|
+
execSync(`lsof -ti:${SERVER_PORT} | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
|
|
55
|
+
// 4. Give Windows a moment to release file handles before npm tries to rename.
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
const end = Date.now() + 1500;
|
|
58
|
+
while (Date.now() < end) { /* spin-wait; Atomics.wait not worth importing */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
13
61
|
|
|
14
62
|
/**
|
|
15
63
|
* Get current version
|
|
@@ -34,7 +82,7 @@ function getCurrentVersion() {
|
|
|
34
82
|
* Compare semver versions
|
|
35
83
|
* Returns true if latest > current
|
|
36
84
|
*/
|
|
37
|
-
function isNewerVersion(current, latest) {
|
|
85
|
+
export function isNewerVersion(current, latest) {
|
|
38
86
|
const currentParts = current.split(".").map(Number);
|
|
39
87
|
const latestParts = latest.split(".").map(Number);
|
|
40
88
|
|
|
@@ -61,12 +109,20 @@ function isRestrictedEnvironment() {
|
|
|
61
109
|
/**
|
|
62
110
|
* Check for npm updates (non-blocking, notification only)
|
|
63
111
|
*/
|
|
112
|
+
/**
|
|
113
|
+
* Kill running 9remote processes so user can safely run `npm i -g 9remote@latest`.
|
|
114
|
+
* Called when user chooses manual update from startup menu.
|
|
115
|
+
*/
|
|
116
|
+
export function stopRunningInstances() {
|
|
117
|
+
cleanupBeforeUpdate();
|
|
118
|
+
}
|
|
119
|
+
|
|
64
120
|
export async function checkForUpdates() {
|
|
65
121
|
try {
|
|
66
122
|
const currentVersion = getCurrentVersion();
|
|
67
123
|
if (!currentVersion) return;
|
|
68
124
|
|
|
69
|
-
const response = await
|
|
125
|
+
const response = await browserFetch(NPM_REGISTRY_URL, {
|
|
70
126
|
signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT)
|
|
71
127
|
});
|
|
72
128
|
|
|
@@ -94,7 +150,7 @@ export async function checkLatestVersion() {
|
|
|
94
150
|
try {
|
|
95
151
|
const currentVersion = getCurrentVersion();
|
|
96
152
|
if (!currentVersion) return null;
|
|
97
|
-
const response = await
|
|
153
|
+
const response = await browserFetch(NPM_REGISTRY_URL, {
|
|
98
154
|
signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT)
|
|
99
155
|
});
|
|
100
156
|
if (!response.ok) return null;
|
|
@@ -158,7 +214,7 @@ export async function checkAndUpdate(skipUpdate = false) {
|
|
|
158
214
|
|
|
159
215
|
startSpinner("Checking for updates...");
|
|
160
216
|
|
|
161
|
-
|
|
217
|
+
browserFetch(NPM_REGISTRY_URL, { signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT) })
|
|
162
218
|
.then((res) => res.json())
|
|
163
219
|
.then((data) => {
|
|
164
220
|
if (resolved) return;
|
|
@@ -191,11 +247,33 @@ export async function checkAndUpdate(skipUpdate = false) {
|
|
|
191
247
|
|
|
192
248
|
let scriptPath, shellCmd;
|
|
193
249
|
|
|
250
|
+
// The update script must kill our tracked processes by PID ONLY.
|
|
251
|
+
// Never use `taskkill /IM <image>` or `pkill -f <name>` — those match
|
|
252
|
+
// by binary name / commandline and would nuke unrelated apps on the
|
|
253
|
+
// machine (other cloudflared tunnels, other tray apps, any node.exe).
|
|
254
|
+
const pidsDir = getPidsDir();
|
|
255
|
+
|
|
194
256
|
if (platform === "win32") {
|
|
195
257
|
const script = `@echo off
|
|
196
258
|
echo 📥 Downloading update...
|
|
197
|
-
echo ⏳
|
|
198
|
-
|
|
259
|
+
echo ⏳ Stopping 9remote processes...
|
|
260
|
+
|
|
261
|
+
REM Kill cloudflared first so it stops reconnecting, then kill the agent
|
|
262
|
+
REM tree (/T also terminates its server child + tray helper). PID-based so
|
|
263
|
+
REM we never touch unrelated node.exe / cloudflared.exe on this machine.
|
|
264
|
+
REM ptyDaemon is intentionally NOT killed — keep terminal sessions alive.
|
|
265
|
+
for %%N in (cloudflared agent) do (
|
|
266
|
+
if exist "${pidsDir}\\%%N.pid" (
|
|
267
|
+
for /f %%P in ('type "${pidsDir}\\%%N.pid"') do taskkill /F /T /PID %%P >nul 2>&1
|
|
268
|
+
del /f /q "${pidsDir}\\%%N.pid" >nul 2>&1
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
REM Safety net: kill anything still bound to our server port.
|
|
273
|
+
for /f "tokens=5" %%a in ('netstat -aon ^| findstr :${SERVER_PORT}') do taskkill /F /PID %%a >nul 2>&1
|
|
274
|
+
|
|
275
|
+
REM Let Windows flush file handles before npm renames node_modules\\9remote.
|
|
276
|
+
timeout /t 3 /nobreak >nul
|
|
199
277
|
|
|
200
278
|
echo 🔄 Installing new version...
|
|
201
279
|
call npm cache clean --force >nul 2>&1
|
|
@@ -218,11 +296,24 @@ if %ERRORLEVEL% EQU 0 (
|
|
|
218
296
|
} else {
|
|
219
297
|
const script = `#!/bin/bash
|
|
220
298
|
echo "📥 Downloading update..."
|
|
221
|
-
echo "⏳
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
299
|
+
echo "⏳ Stopping 9remote processes..."
|
|
300
|
+
|
|
301
|
+
# Kill cloudflared first, then the agent (children die with the agent on
|
|
302
|
+
# POSIX once the parent process exits and systemd/init reaps them, or here
|
|
303
|
+
# because we SIGKILL them via kill -9). PID-based so unrelated apps are safe.
|
|
304
|
+
# ptyDaemon is intentionally NOT killed — keep terminal sessions alive.
|
|
305
|
+
for name in cloudflared agent; do
|
|
306
|
+
f="${pidsDir}/\${name}.pid"
|
|
307
|
+
if [ -f "$f" ]; then
|
|
308
|
+
pid=$(cat "$f" 2>/dev/null)
|
|
309
|
+
[ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true
|
|
310
|
+
rm -f "$f"
|
|
311
|
+
fi
|
|
312
|
+
done
|
|
313
|
+
|
|
314
|
+
# Safety net: kill anything still bound to our server port.
|
|
315
|
+
lsof -ti:${SERVER_PORT} | xargs kill -9 2>/dev/null || true
|
|
316
|
+
sleep 2
|
|
226
317
|
|
|
227
318
|
echo "🔄 Installing new version..."
|
|
228
319
|
npm cache clean --force 2>/dev/null
|
|
@@ -247,6 +338,9 @@ fi
|
|
|
247
338
|
shellCmd = ["sh", [scriptPath]];
|
|
248
339
|
}
|
|
249
340
|
|
|
341
|
+
// Cleanup child processes to release file locks before npm install
|
|
342
|
+
cleanupBeforeUpdate();
|
|
343
|
+
|
|
250
344
|
// Execute update script in background
|
|
251
345
|
const child = spawn(shellCmd[0], shellCmd[1], {
|
|
252
346
|
detached: true,
|
|
Binary file
|