| import { loadState, saveState, generateShortId } from "../shared/state.js"; |
| import { spawnQuickTunnel, killCloudflared, isCloudflaredRunning, setUnexpectedExitHandler } from "./cloudflared.js"; |
| import { clearPid } from "./pid.js"; |
| import { waitForHealth, probeUrlAlive } from "./healthCheck.js"; |
| import { WORKER_URL } from "./config.js"; |
| import { getSettings, updateSettings } from "@/lib/localDb"; |
|
|
| const svc = { |
| cancelToken: { cancelled: false }, |
| spawnInProgress: false, |
| lastRestartAt: 0, |
| activeLocalPort: null, |
| }; |
|
|
| export function getTunnelService() { return svc; } |
| export function isTunnelManuallyDisabled() { return svc.cancelToken.cancelled; } |
| export function isTunnelReconnecting() { return svc.spawnInProgress; } |
|
|
| let onUnexpectedExit = null; |
| export function setTunnelUnexpectedExitCallback(cb) { onUnexpectedExit = cb; } |
|
|
| async function registerTunnelUrl(shortId, tunnelUrl) { |
| await fetch(`${WORKER_URL}/api/tunnel/register`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ shortId, tunnelUrl }) |
| }); |
| } |
|
|
| function throwIfCancelled(token) { |
| if (token.cancelled) throw new Error("tunnel cancelled"); |
| } |
|
|
| export async function enableTunnel(localPort = 20128) { |
| console.log(`[Tunnel] enable start (port=${localPort})`); |
| svc.cancelToken = { cancelled: false }; |
| svc.activeLocalPort = localPort; |
| svc.spawnInProgress = true; |
| const token = svc.cancelToken; |
|
|
| try { |
| if (isCloudflaredRunning()) { |
| const existing = loadState(); |
| if (existing?.tunnelUrl && existing?.shortId) { |
| const publicUrl = `https://r${existing.shortId}.abc-tunnel.us`; |
| |
| const [directOk, publicOk] = await Promise.all([ |
| probeUrlAlive(existing.tunnelUrl), |
| probeUrlAlive(publicUrl), |
| ]); |
| if (directOk && publicOk) { |
| console.log(`[Tunnel] already running, reuse: ${existing.tunnelUrl}`); |
| return { success: true, tunnelUrl: existing.tunnelUrl, shortId: existing.shortId, publicUrl, alreadyRunning: true }; |
| } |
| console.log(`[Tunnel] stale (direct=${directOk} public=${publicOk}), respawn`); |
| } |
| } |
|
|
| killCloudflared(localPort); |
| console.log("[Tunnel] killed existing cloudflared"); |
| throwIfCancelled(token); |
|
|
| const existing = loadState(); |
| const shortId = existing?.shortId || generateShortId(); |
|
|
| const onUrlUpdate = async (url) => { |
| if (token.cancelled) return; |
| console.log(`[Tunnel] url updated: ${url}`); |
| await registerTunnelUrl(shortId, url); |
| saveState({ shortId, tunnelUrl: url }); |
| await updateSettings({ tunnelEnabled: true, tunnelUrl: url }); |
| }; |
|
|
| |
| setUnexpectedExitHandler(() => { |
| console.warn("[Tunnel] cloudflared exited unexpectedly, scheduling respawn"); |
| if (onUnexpectedExit) onUnexpectedExit(); |
| }); |
|
|
| const { tunnelUrl } = await spawnQuickTunnel(localPort, onUrlUpdate); |
| console.log(`[Tunnel] spawned: ${tunnelUrl}`); |
| throwIfCancelled(token); |
|
|
| const publicUrl = `https://r${shortId}.abc-tunnel.us`; |
| await registerTunnelUrl(shortId, tunnelUrl); |
| saveState({ shortId, tunnelUrl }); |
| await updateSettings({ tunnelEnabled: true, tunnelUrl }); |
| console.log(`[Tunnel] registered shortId=${shortId} publicUrl=${publicUrl}`); |
|
|
| |
| await waitForHealth(publicUrl, token); |
| console.log("[Tunnel] public URL healthy"); |
| |
| if (!(await probeUrlAlive(tunnelUrl))) { |
| console.warn("[Tunnel] direct URL not reachable yet, continuing via publicUrl"); |
| } else { |
| console.log("[Tunnel] direct URL healthy"); |
| } |
|
|
| console.log("[Tunnel] enable success"); |
| return { success: true, tunnelUrl, shortId, publicUrl }; |
| } catch (e) { |
| |
| if (!/cloudflared killed|tunnel cancelled/.test(e.message)) { |
| console.error(`[Tunnel] enable error: ${e.message}`); |
| } |
| throw e; |
| } finally { |
| svc.spawnInProgress = false; |
| } |
| } |
|
|
| export async function disableTunnel() { |
| console.log("[Tunnel] disable"); |
| |
| svc.cancelToken.cancelled = true; |
| setUnexpectedExitHandler(null); |
|
|
| try { killCloudflared(svc.activeLocalPort); } catch (e) { console.warn(`[Tunnel] kill warn: ${e.message}`); } |
| clearPid(); |
|
|
| const state = loadState(); |
| if (state) saveState({ shortId: state.shortId, tunnelUrl: null }); |
|
|
| await updateSettings({ tunnelEnabled: false, tunnelUrl: "" }); |
| |
| svc.spawnInProgress = false; |
| svc.activeLocalPort = null; |
| return { success: true }; |
| } |
|
|
| export async function getTunnelStatus() { |
| const settings = await getSettings(); |
| const settingsEnabled = settings.tunnelEnabled === true; |
| const state = loadState(); |
| const shortId = state?.shortId || ""; |
| const publicUrl = shortId ? `https://r${shortId}.abc-tunnel.us` : ""; |
| const tunnelUrl = state?.tunnelUrl || ""; |
|
|
| |
| const running = settingsEnabled ? isCloudflaredRunning() : false; |
|
|
| return { |
| enabled: settingsEnabled && running, |
| settingsEnabled, |
| tunnelUrl, |
| shortId, |
| publicUrl, |
| running |
| }; |
| } |
|
|