9remote 0.1.13 ā 0.1.15
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.cjs +70 -86
- package/dist/server.cjs +39 -20
- package/index.js +17 -23
- package/package.json +1 -1
- package/utils/cloudflared.js +50 -3
- package/utils/installer.js +0 -137
package/index.js
CHANGED
|
@@ -13,8 +13,7 @@ import { generateApiKeyWithMachine } from "./utils/apiKey.js";
|
|
|
13
13
|
import { loadKey, saveKey, saveState, clearState } from "./utils/state.js";
|
|
14
14
|
import { createTempKey } from "./utils/token.js";
|
|
15
15
|
import { checkForUpdates } from "./utils/updateChecker.js";
|
|
16
|
-
import {
|
|
17
|
-
import { ensureCloudflared, spawnCloudflared, killCloudflared } from "./utils/cloudflared.js";
|
|
16
|
+
import { ensureCloudflared, spawnCloudflared, killCloudflared, resetRestartCounter } from "./utils/cloudflared.js";
|
|
18
17
|
|
|
19
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
19
|
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
@@ -174,6 +173,7 @@ function setupExitHandler(serverManager, tunnelProcess, apiKey) {
|
|
|
174
173
|
console.log(chalk.yellow("\n\nš Stopping server..."));
|
|
175
174
|
serverManager.shutdown();
|
|
176
175
|
tunnelProcess.kill();
|
|
176
|
+
resetRestartCounter();
|
|
177
177
|
|
|
178
178
|
// Cleanup tunnel on worker
|
|
179
179
|
try {
|
|
@@ -214,14 +214,9 @@ async function createNamedTunnel(apiKey) {
|
|
|
214
214
|
async function startServerAndTunnel(selectedKey) {
|
|
215
215
|
console.log(chalk.cyan("\nš Starting server..."));
|
|
216
216
|
|
|
217
|
-
// Kill existing
|
|
217
|
+
// Kill existing cloudflared process
|
|
218
218
|
try {
|
|
219
219
|
killCloudflared();
|
|
220
|
-
if (process.platform === "win32") {
|
|
221
|
-
execSync("taskkill /F /IM node.exe /FI \"WINDOWTITLE eq server.js*\" 2>nul || exit 0", { stdio: "ignore" });
|
|
222
|
-
} else {
|
|
223
|
-
execSync("pkill -f 'node server.js' 2>/dev/null || true", { stdio: "ignore" });
|
|
224
|
-
}
|
|
225
220
|
} catch { }
|
|
226
221
|
|
|
227
222
|
// Create session first
|
|
@@ -266,13 +261,22 @@ async function startServerAndTunnel(selectedKey) {
|
|
|
266
261
|
|
|
267
262
|
const { token, hostname: tunnelUrl } = tunnelData;
|
|
268
263
|
|
|
269
|
-
// Spawn cloudflared with token
|
|
264
|
+
// Spawn cloudflared with token and auto-restart callback
|
|
270
265
|
console.log(chalk.cyan("ā
Starting tunnel..."));
|
|
271
266
|
let tunnelProcess;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
267
|
+
|
|
268
|
+
const startTunnel = async (tunnelToken) => {
|
|
269
|
+
try {
|
|
270
|
+
tunnelProcess = await spawnCloudflared(tunnelToken, startTunnel);
|
|
271
|
+
return tunnelProcess;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.log(chalk.red(`ā Failed to start cloudflared: ${error.message}`));
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
tunnelProcess = await startTunnel(token);
|
|
279
|
+
if (!tunnelProcess) {
|
|
276
280
|
serverManager.shutdown();
|
|
277
281
|
return null;
|
|
278
282
|
}
|
|
@@ -522,16 +526,6 @@ async function autoStartDev() {
|
|
|
522
526
|
async function start() {
|
|
523
527
|
checkForUpdates();
|
|
524
528
|
|
|
525
|
-
// Ensure native dependencies are installed (first time only)
|
|
526
|
-
const depsReady = await ensureNativeDeps();
|
|
527
|
-
if (!depsReady) {
|
|
528
|
-
console.log(chalk.red("ā Failed to install dependencies. Please try again."));
|
|
529
|
-
process.exit(1);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Wait a bit for symlinks to be fully created (filesystem sync)
|
|
533
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
534
|
-
|
|
535
529
|
if (process.argv.includes("--auto")) {
|
|
536
530
|
await autoStartDev();
|
|
537
531
|
} else {
|
package/package.json
CHANGED
package/utils/cloudflared.js
CHANGED
|
@@ -11,6 +11,15 @@ const BIN_NAME = IS_WINDOWS ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
|
11
11
|
const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
|
|
12
12
|
const PID_FILE = path.join(os.homedir(), ".9remote", "cloudflared.pid");
|
|
13
13
|
|
|
14
|
+
// Track intentional shutdown to suppress exit logs
|
|
15
|
+
let isIntentionalShutdown = false;
|
|
16
|
+
|
|
17
|
+
// Auto-restart configuration
|
|
18
|
+
const MAX_RESTART_ATTEMPTS = 3;
|
|
19
|
+
const RESTART_WINDOW_MS = 60000; // 1 minute
|
|
20
|
+
let restartTimes = [];
|
|
21
|
+
let restartCallback = null;
|
|
22
|
+
|
|
14
23
|
const GITHUB_BASE_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
15
24
|
|
|
16
25
|
/**
|
|
@@ -152,11 +161,17 @@ const LOG_IGNORE = [
|
|
|
152
161
|
/**
|
|
153
162
|
* Spawn cloudflared tunnel
|
|
154
163
|
* @param {string} tunnelToken
|
|
164
|
+
* @param {Function} onRestart - Callback when tunnel needs restart
|
|
155
165
|
* @returns {ChildProcess}
|
|
156
166
|
*/
|
|
157
|
-
export async function spawnCloudflared(tunnelToken) {
|
|
167
|
+
export async function spawnCloudflared(tunnelToken, onRestart = null) {
|
|
158
168
|
const binaryPath = await ensureCloudflared();
|
|
159
169
|
|
|
170
|
+
// Store restart callback
|
|
171
|
+
if (onRestart) {
|
|
172
|
+
restartCallback = onRestart;
|
|
173
|
+
}
|
|
174
|
+
|
|
160
175
|
const child = spawn(binaryPath, ["tunnel", "run", "--token", tunnelToken], {
|
|
161
176
|
detached: false,
|
|
162
177
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -172,6 +187,11 @@ export async function spawnCloudflared(tunnelToken) {
|
|
|
172
187
|
return;
|
|
173
188
|
}
|
|
174
189
|
|
|
190
|
+
// Skip errors during intentional shutdown
|
|
191
|
+
if (isIntentionalShutdown) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
175
195
|
// Show connection status briefly
|
|
176
196
|
if (msg.includes("Registered tunnel connection")) {
|
|
177
197
|
connectionCount++;
|
|
@@ -198,8 +218,27 @@ export async function spawnCloudflared(tunnelToken) {
|
|
|
198
218
|
});
|
|
199
219
|
|
|
200
220
|
child.on("exit", (code) => {
|
|
201
|
-
|
|
221
|
+
// Only log unexpected exits
|
|
222
|
+
if (!isIntentionalShutdown && code !== 0 && code !== null) {
|
|
202
223
|
console.log(`cloudflared exited with code ${code}`);
|
|
224
|
+
|
|
225
|
+
// Auto-restart logic
|
|
226
|
+
if (restartCallback) {
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
restartTimes.push(now);
|
|
229
|
+
|
|
230
|
+
// Remove old restart times outside window
|
|
231
|
+
restartTimes = restartTimes.filter(t => t > now - RESTART_WINDOW_MS);
|
|
232
|
+
|
|
233
|
+
if (restartTimes.length <= MAX_RESTART_ATTEMPTS) {
|
|
234
|
+
console.log(`š Restarting tunnel... (attempt ${restartTimes.length}/${MAX_RESTART_ATTEMPTS})`);
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
restartCallback(tunnelToken);
|
|
237
|
+
}, 2000);
|
|
238
|
+
} else {
|
|
239
|
+
console.log(`ā Too many tunnel restarts (${MAX_RESTART_ATTEMPTS} in ${RESTART_WINDOW_MS / 1000}s). Giving up.`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
203
242
|
}
|
|
204
243
|
});
|
|
205
244
|
|
|
@@ -215,12 +254,20 @@ export async function spawnCloudflared(tunnelToken) {
|
|
|
215
254
|
export function killCloudflared() {
|
|
216
255
|
try {
|
|
217
256
|
if (fs.existsSync(PID_FILE)) {
|
|
257
|
+
isIntentionalShutdown = true;
|
|
218
258
|
const pid = parseInt(fs.readFileSync(PID_FILE, "utf8"));
|
|
219
259
|
process.kill(pid);
|
|
220
260
|
fs.unlinkSync(PID_FILE);
|
|
221
|
-
console.log("ā
cloudflared stopped");
|
|
222
261
|
}
|
|
223
262
|
} catch (error) {
|
|
224
263
|
// Silently ignore errors
|
|
225
264
|
}
|
|
226
265
|
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reset restart counter
|
|
269
|
+
*/
|
|
270
|
+
export function resetRestartCounter() {
|
|
271
|
+
restartTimes = [];
|
|
272
|
+
restartCallback = null;
|
|
273
|
+
}
|
package/utils/installer.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
2
|
-
import ora from "ora";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
|
-
import os from "os";
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const PROJECT_ROOT = path.resolve(__dirname, "../..");
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Check if running from global installation
|
|
14
|
-
*/
|
|
15
|
-
function isGlobalInstall() {
|
|
16
|
-
// Check if we're in global node_modules
|
|
17
|
-
return PROJECT_ROOT.includes("/node_modules/9remote") ||
|
|
18
|
-
PROJECT_ROOT.includes("\\node_modules\\9remote");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get dependencies directory
|
|
23
|
-
* For global install: use ~/.9remote/node_modules
|
|
24
|
-
* For local dev: use project node_modules
|
|
25
|
-
*/
|
|
26
|
-
function getDepsDir() {
|
|
27
|
-
if (isGlobalInstall()) {
|
|
28
|
-
const homeDir = os.homedir();
|
|
29
|
-
return path.join(homeDir, ".9remote");
|
|
30
|
-
}
|
|
31
|
-
return PROJECT_ROOT;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const DEPS_DIR = getDepsDir();
|
|
35
|
-
const INSTALL_MARKER = path.join(DEPS_DIR, "node_modules", ".deps-installed");
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Ensure all dependencies are installed
|
|
39
|
-
* Only runs once on first launch
|
|
40
|
-
*/
|
|
41
|
-
export async function ensureNativeDeps() {
|
|
42
|
-
// Create deps directory if needed
|
|
43
|
-
if (!fs.existsSync(DEPS_DIR)) {
|
|
44
|
-
fs.mkdirSync(DEPS_DIR, { recursive: true });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check if already installed
|
|
48
|
-
if (fs.existsSync(INSTALL_MARKER)) {
|
|
49
|
-
// Add deps to NODE_PATH for global install
|
|
50
|
-
if (isGlobalInstall()) {
|
|
51
|
-
const depsNodeModules = path.join(DEPS_DIR, "node_modules");
|
|
52
|
-
if (process.env.NODE_PATH) {
|
|
53
|
-
process.env.NODE_PATH = `${depsNodeModules}${path.delimiter}${process.env.NODE_PATH}`;
|
|
54
|
-
} else {
|
|
55
|
-
process.env.NODE_PATH = depsNodeModules;
|
|
56
|
-
}
|
|
57
|
-
require("module").Module._initPaths();
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const spinner = ora("Checking dependencies...").start();
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
// Check if cloudflared is installed
|
|
66
|
-
let allInstalled = true;
|
|
67
|
-
try {
|
|
68
|
-
await import("cloudflared");
|
|
69
|
-
} catch {
|
|
70
|
-
allInstalled = false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (allInstalled) {
|
|
74
|
-
spinner.succeed("Dependencies ready");
|
|
75
|
-
|
|
76
|
-
// Mark as installed
|
|
77
|
-
fs.mkdirSync(path.dirname(INSTALL_MARKER), { recursive: true });
|
|
78
|
-
fs.writeFileSync(INSTALL_MARKER, new Date().toISOString());
|
|
79
|
-
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Install cloudflared only (other deps are in package.json)
|
|
84
|
-
spinner.stop();
|
|
85
|
-
console.log(chalk.cyan("š¦ Installing cloudflared (first time only, ~10 seconds)...\n"));
|
|
86
|
-
|
|
87
|
-
const packageNames = ["cloudflared"];
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
console.log(chalk.gray(" Installing cloudflared..."));
|
|
91
|
-
|
|
92
|
-
// Create package.json if in global install mode
|
|
93
|
-
if (isGlobalInstall()) {
|
|
94
|
-
const pkgJsonPath = path.join(DEPS_DIR, "package.json");
|
|
95
|
-
if (!fs.existsSync(pkgJsonPath)) {
|
|
96
|
-
fs.writeFileSync(pkgJsonPath, JSON.stringify({
|
|
97
|
-
name: "9remote-deps",
|
|
98
|
-
version: "1.0.0",
|
|
99
|
-
private: true
|
|
100
|
-
}, null, 2));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
execSync(`npm install --no-save --prefer-offline ${packageNames.join(" ")}`, {
|
|
105
|
-
cwd: DEPS_DIR,
|
|
106
|
-
stdio: "inherit"
|
|
107
|
-
});
|
|
108
|
-
console.log(chalk.green("\nā Cloudflared installed successfully"));
|
|
109
|
-
|
|
110
|
-
// Add deps to NODE_PATH for global install
|
|
111
|
-
if (isGlobalInstall()) {
|
|
112
|
-
const depsNodeModules = path.join(DEPS_DIR, "node_modules");
|
|
113
|
-
if (process.env.NODE_PATH) {
|
|
114
|
-
process.env.NODE_PATH = `${depsNodeModules}${path.delimiter}${process.env.NODE_PATH}`;
|
|
115
|
-
} else {
|
|
116
|
-
process.env.NODE_PATH = depsNodeModules;
|
|
117
|
-
}
|
|
118
|
-
require("module").Module._initPaths();
|
|
119
|
-
}
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.log(chalk.red("\nā Failed to install dependencies"));
|
|
122
|
-
console.error(error.message);
|
|
123
|
-
console.log(chalk.yellow("\nš” Tip: Try running with sudo if permission denied"));
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Mark as installed
|
|
128
|
-
fs.mkdirSync(path.dirname(INSTALL_MARKER), { recursive: true });
|
|
129
|
-
fs.writeFileSync(INSTALL_MARKER, new Date().toISOString());
|
|
130
|
-
|
|
131
|
-
return true;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
spinner.fail("Failed to install dependencies");
|
|
134
|
-
console.error(error.message);
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
}
|