Fix bug Tunnel

This commit is contained in:
decolua
2026-02-22 21:44:11 +07:00
parent a5eb5a864e
commit d21f7aaadc
9 changed files with 734 additions and 237 deletions

View File

@@ -113,6 +113,12 @@ export async function ensureCloudflared() {
}
let cloudflaredProcess = null;
let unexpectedExitHandler = null;
/** Register a callback to be called when cloudflared exits unexpectedly after connecting */
export function setUnexpectedExitHandler(handler) {
unexpectedExitHandler = handler;
}
export async function spawnCloudflared(tunnelToken) {
const binaryPath = await ensureCloudflared();
@@ -127,7 +133,9 @@ export async function spawnCloudflared(tunnelToken) {
return new Promise((resolve, reject) => {
let connectionCount = 0;
let resolved = false;
const timeout = setTimeout(() => {
resolved = true;
resolve(child);
}, 90000);
@@ -135,7 +143,8 @@ export async function spawnCloudflared(tunnelToken) {
const msg = data.toString();
if (msg.includes("Registered tunnel connection")) {
connectionCount++;
if (connectionCount >= 4) {
if (connectionCount >= 4 && !resolved) {
resolved = true;
clearTimeout(timeout);
resolve(child);
}
@@ -146,14 +155,27 @@ export async function spawnCloudflared(tunnelToken) {
child.stderr.on("data", handleLog);
child.on("error", (err) => {
clearTimeout(timeout);
reject(err);
if (!resolved) {
resolved = true;
clearTimeout(timeout);
reject(err);
}
});
child.on("exit", (code) => {
clearTimeout(timeout);
if (connectionCount === 0) {
reject(new Error(`cloudflared exited with code ${code}`));
cloudflaredProcess = null;
clearPid();
if (!resolved) {
resolved = true;
clearTimeout(timeout);
if (connectionCount === 0) {
reject(new Error(`cloudflared exited with code ${code}`));
return;
}
}
// Notify reconnect handler if tunnel died after successful connection
if (unexpectedExitHandler) {
unexpectedExitHandler();
}
});
});

View File

@@ -1,6 +1,6 @@
import crypto from "crypto";
import { loadState, saveState, clearState } from "./state.js";
import { spawnCloudflared, killCloudflared, isCloudflaredRunning } from "./cloudflared.js";
import { spawnCloudflared, killCloudflared, isCloudflaredRunning, setUnexpectedExitHandler } from "./cloudflared.js";
import { getSettings, updateSettings } from "@/lib/localDb";
const TUNNEL_WORKER_URL = process.env.TUNNEL_WORKER_URL || "https://tunnel.9router.com";
@@ -8,6 +8,9 @@ const MACHINE_ID_SALT = "9router-tunnel-salt";
const API_KEY_SECRET = "9router-tunnel-api-key-secret";
const SHORT_ID_LENGTH = 6;
const SHORT_ID_CHARS = "abcdefghijklmnpqrstuvwxyz23456789";
const RECONNECT_DELAYS_MS = [5000, 15000, 30000];
let isReconnecting = false;
function generateShortId() {
let result = "";
@@ -80,9 +83,43 @@ export async function enableTunnel() {
await updateSettings({ tunnelEnabled: true, tunnelUrl: hostname });
// Register exit handler for auto-reconnect on unexpected crash/sleep-wake
setUnexpectedExitHandler(() => scheduleReconnect(0));
return { success: true, tunnelUrl: hostname, shortId };
}
async function scheduleReconnect(attempt) {
if (isReconnecting) return;
isReconnecting = true;
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
console.log(`[Tunnel] Unexpected exit detected, reconnecting in ${delay / 1000}s (attempt ${attempt + 1})...`);
await new Promise((r) => setTimeout(r, delay));
try {
const settings = await getSettings();
if (!settings.tunnelEnabled) {
console.log("[Tunnel] Tunnel disabled, skipping reconnect");
isReconnecting = false;
return;
}
await enableTunnel();
console.log("[Tunnel] Reconnected successfully");
isReconnecting = false;
} catch (err) {
console.log(`[Tunnel] Reconnect attempt ${attempt + 1} failed:`, err.message);
isReconnecting = false;
const nextAttempt = attempt + 1;
if (nextAttempt < RECONNECT_DELAYS_MS.length) {
scheduleReconnect(nextAttempt);
} else {
console.log("[Tunnel] All reconnect attempts exhausted");
}
}
}
export async function disableTunnel() {
const state = loadState();