mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Refactor error handling and localDb structure, and fix usage tracking bug.
This commit is contained in:
@@ -1,3 +1,18 @@
|
|||||||
|
# v0.3.91 (2026-04-15)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Add Kiro AWS Identity Center device flow for provider OAuth
|
||||||
|
- Add TTS (Text-to-Speech) core handler and TTS models config
|
||||||
|
- Add media providers dashboard page
|
||||||
|
- Add suggested models API endpoint
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
- Refactor error handling to config-driven approach with centralized error rules
|
||||||
|
- Refactor localDb and usageDb for cleaner structure
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- Fix usage tracking bug
|
||||||
|
|
||||||
# v0.3.90 (2026-04-14)
|
# v0.3.90 (2026-04-14)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.3.91",
|
"version": "0.3.94",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export default function ClaudeToolCard({
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
filename: "~/.claude/settings.json",
|
filename: "~/.claude/settings.json",
|
||||||
content: JSON.stringify({ env }, null, 2),
|
content: JSON.stringify({ hasCompletedOnboarding: true, env }, null, 2),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -226,8 +226,30 @@ export default function APIPageClient({ machineId }) {
|
|||||||
setTunnelLoading(true);
|
setTunnelLoading(true);
|
||||||
setTunnelStatus(null);
|
setTunnelStatus(null);
|
||||||
setTunnelProgress("Creating tunnel...");
|
setTunnelProgress("Creating tunnel...");
|
||||||
|
|
||||||
|
// Poll download progress while enable request is pending
|
||||||
|
let polling = true;
|
||||||
|
const pollProgress = async () => {
|
||||||
|
while (polling) {
|
||||||
|
try {
|
||||||
|
const r = await fetch("/api/tunnel/status");
|
||||||
|
if (r.ok) {
|
||||||
|
const s = await r.json();
|
||||||
|
if (s.download?.downloading) {
|
||||||
|
setTunnelProgress(`Downloading cloudflared... ${s.download.progress}%`);
|
||||||
|
} else if (polling) {
|
||||||
|
setTunnelProgress("Creating tunnel...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pollProgress();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/tunnel/enable", { method: "POST" });
|
const res = await fetch("/api/tunnel/enable", { method: "POST" });
|
||||||
|
polling = false;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setTunnelStatus({ type: "error", message: data.error || "Failed to enable tunnel" });
|
setTunnelStatus({ type: "error", message: data.error || "Failed to enable tunnel" });
|
||||||
@@ -246,6 +268,7 @@ export default function APIPageClient({ machineId }) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setTunnelStatus({ type: "error", message: error.message });
|
setTunnelStatus({ type: "error", message: error.message });
|
||||||
} finally {
|
} finally {
|
||||||
|
polling = false;
|
||||||
setTunnelLoading(false);
|
setTunnelLoading(false);
|
||||||
setTunnelProgress("");
|
setTunnelProgress("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export async function POST(request) {
|
|||||||
// Merge new env with existing settings
|
// Merge new env with existing settings
|
||||||
const newSettings = {
|
const newSettings = {
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
|
hasCompletedOnboarding: true,
|
||||||
env: {
|
env: {
|
||||||
...(currentSettings.env || {}),
|
...(currentSettings.env || {}),
|
||||||
...env,
|
...env,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { getTunnelStatus, getTailscaleStatus } from "@/lib/tunnel/tunnelManager";
|
import { getTunnelStatus, getTailscaleStatus } from "@/lib/tunnel/tunnelManager";
|
||||||
|
import { getDownloadStatus } from "@/lib/tunnel/cloudflared";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]);
|
const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]);
|
||||||
return NextResponse.json({ tunnel, tailscale });
|
const download = getDownloadStatus();
|
||||||
|
return NextResponse.json({ tunnel, tailscale, download });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Tunnel status error:", error);
|
console.error("Tunnel status error:", error);
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
|
||||||
|
const APP_NAME = "9router";
|
||||||
|
|
||||||
|
export function getDataDir() {
|
||||||
|
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), APP_NAME);
|
||||||
|
}
|
||||||
|
return path.join(os.homedir(), `.${APP_NAME}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATA_DIR = getDataDir();
|
||||||
+1
-21
@@ -2,32 +2,12 @@ import { Low } from "lowdb";
|
|||||||
import { JSONFile } from "lowdb/node";
|
import { JSONFile } from "lowdb/node";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import lockfile from "proper-lockfile";
|
import lockfile from "proper-lockfile";
|
||||||
|
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||||
|
|
||||||
const DEFAULT_MITM_ROUTER_BASE = "http://localhost:20128";
|
const DEFAULT_MITM_ROUTER_BASE = "http://localhost:20128";
|
||||||
const isCloud = typeof caches !== 'undefined' || typeof caches === 'object';
|
const isCloud = typeof caches !== 'undefined' || typeof caches === 'object';
|
||||||
|
|
||||||
function getAppName() {
|
|
||||||
return "9router";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserDataDir() {
|
|
||||||
if (isCloud) return "/tmp";
|
|
||||||
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
|
||||||
|
|
||||||
const platform = process.platform;
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
const appName = getAppName();
|
|
||||||
|
|
||||||
if (platform === "win32") {
|
|
||||||
return path.join(process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), appName);
|
|
||||||
}
|
|
||||||
return path.join(homeDir, `.${appName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATA_DIR = getUserDataDir();
|
|
||||||
const DB_FILE = isCloud ? null : path.join(DATA_DIR, "db.json");
|
const DB_FILE = isCloud ? null : path.join(DATA_DIR, "db.json");
|
||||||
|
|
||||||
if (!isCloud && !fs.existsSync(DATA_DIR)) {
|
if (!isCloud && !fs.existsSync(DATA_DIR)) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Low } from "lowdb";
|
import { Low } from "lowdb";
|
||||||
import { JSONFile } from "lowdb/node";
|
import { JSONFile } from "lowdb/node";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||||
|
|
||||||
const isCloud = typeof caches !== "undefined" && typeof caches === "object";
|
const isCloud = typeof caches !== "undefined" && typeof caches === "object";
|
||||||
|
|
||||||
@@ -12,26 +12,6 @@ const DEFAULT_FLUSH_INTERVAL_MS = 5000;
|
|||||||
const DEFAULT_MAX_JSON_SIZE = 5 * 1024; // 5KB default, configurable via settings
|
const DEFAULT_MAX_JSON_SIZE = 5 * 1024; // 5KB default, configurable via settings
|
||||||
const CONFIG_CACHE_TTL_MS = 5000;
|
const CONFIG_CACHE_TTL_MS = 5000;
|
||||||
const MAX_TOTAL_DB_SIZE = 50 * 1024 * 1024; // 50MB hard limit for total DB file
|
const MAX_TOTAL_DB_SIZE = 50 * 1024 * 1024; // 50MB hard limit for total DB file
|
||||||
|
|
||||||
function getAppName() {
|
|
||||||
return "9router";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserDataDir() {
|
|
||||||
if (isCloud) return "/tmp";
|
|
||||||
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
|
||||||
|
|
||||||
const platform = process.platform;
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
const appName = getAppName();
|
|
||||||
|
|
||||||
if (platform === "win32") {
|
|
||||||
return path.join(process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), appName);
|
|
||||||
}
|
|
||||||
return path.join(homeDir, `.${appName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATA_DIR = getUserDataDir();
|
|
||||||
const DB_FILE = isCloud ? null : path.join(DATA_DIR, "request-details.json");
|
const DB_FILE = isCloud ? null : path.join(DATA_DIR, "request-details.json");
|
||||||
|
|
||||||
if (!isCloud && !fs.existsSync(DATA_DIR)) {
|
if (!isCloud && !fs.existsSync(DATA_DIR)) {
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import https from "https";
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
import { execSync, spawn } from "child_process";
|
import { execSync, spawn } from "child_process";
|
||||||
import { savePid, loadPid, clearPid } from "./state.js";
|
import { savePid, loadPid, clearPid } from "./state.js";
|
||||||
|
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||||
|
|
||||||
const BIN_DIR = path.join(os.homedir(), ".9router", "bin");
|
const BIN_DIR = path.join(DATA_DIR, "bin");
|
||||||
const BINARY_NAME = "cloudflared";
|
const BINARY_NAME = "cloudflared";
|
||||||
const IS_WINDOWS = os.platform() === "win32";
|
const IS_WINDOWS = os.platform() === "win32";
|
||||||
const BIN_NAME = IS_WINDOWS ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
const BIN_NAME = IS_WINDOWS ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
||||||
const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
|
const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
|
||||||
|
|
||||||
const CLOUDFLARED_VERSION = "2026.3.0";
|
const GITHUB_BASE_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
||||||
const GITHUB_BASE_URL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}`;
|
|
||||||
|
|
||||||
const PLATFORM_MAPPINGS = {
|
const PLATFORM_MAPPINGS = {
|
||||||
darwin: {
|
darwin: {
|
||||||
@@ -21,7 +21,8 @@ const PLATFORM_MAPPINGS = {
|
|||||||
},
|
},
|
||||||
win32: {
|
win32: {
|
||||||
x64: "cloudflared-windows-amd64.exe",
|
x64: "cloudflared-windows-amd64.exe",
|
||||||
x32: "cloudflared-windows-386.exe"
|
ia32: "cloudflared-windows-386.exe",
|
||||||
|
arm64: "cloudflared-windows-386.exe"
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
x64: "cloudflared-linux-amd64",
|
x64: "cloudflared-linux-amd64",
|
||||||
@@ -29,6 +30,13 @@ const PLATFORM_MAPPINGS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fallback order: prefer smallest/most-compatible binary per platform
|
||||||
|
const PLATFORM_FALLBACK = {
|
||||||
|
darwin: "cloudflared-darwin-amd64.tgz",
|
||||||
|
win32: "cloudflared-windows-386.exe",
|
||||||
|
linux: "cloudflared-linux-amd64"
|
||||||
|
};
|
||||||
|
|
||||||
function getDownloadUrl() {
|
function getDownloadUrl() {
|
||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
const arch = os.arch();
|
const arch = os.arch();
|
||||||
@@ -38,20 +46,23 @@ function getDownloadUrl() {
|
|||||||
throw new Error(`Unsupported platform: ${platform}`);
|
throw new Error(`Unsupported platform: ${platform}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryName = platformMapping[arch];
|
const binaryName = platformMapping[arch] || PLATFORM_FALLBACK[platform];
|
||||||
if (!binaryName) {
|
|
||||||
throw new Error(`Unsupported architecture: ${arch} for platform ${platform}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${GITHUB_BASE_URL}/${binaryName}`;
|
return `${GITHUB_BASE_URL}/${binaryName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download state — shared so status API can read it
|
||||||
|
const dlState = { downloading: false, progress: 0 };
|
||||||
|
|
||||||
|
export function getDownloadStatus() {
|
||||||
|
return { downloading: dlState.downloading, progress: dlState.progress };
|
||||||
|
}
|
||||||
|
|
||||||
function downloadFile(url, dest) {
|
function downloadFile(url, dest) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const file = fs.createWriteStream(dest);
|
const file = fs.createWriteStream(dest);
|
||||||
|
|
||||||
https.get(url, (response) => {
|
https.get(url, (response) => {
|
||||||
if ([301, 302].includes(response.statusCode)) {
|
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
|
||||||
file.close();
|
file.close();
|
||||||
fs.unlinkSync(dest);
|
fs.unlinkSync(dest);
|
||||||
downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
||||||
@@ -65,18 +76,34 @@ function downloadFile(url, dest) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalBytes = parseInt(response.headers["content-length"], 10) || 0;
|
||||||
|
let receivedBytes = 0;
|
||||||
|
dlState.downloading = true;
|
||||||
|
dlState.progress = 0;
|
||||||
|
|
||||||
|
response.on("data", (chunk) => {
|
||||||
|
receivedBytes += chunk.length;
|
||||||
|
if (totalBytes > 0) dlState.progress = Math.round((receivedBytes / totalBytes) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
response.pipe(file);
|
response.pipe(file);
|
||||||
|
|
||||||
file.on("finish", () => {
|
file.on("finish", () => {
|
||||||
|
dlState.downloading = false;
|
||||||
|
dlState.progress = 100;
|
||||||
file.close(() => resolve(dest));
|
file.close(() => resolve(dest));
|
||||||
});
|
});
|
||||||
|
|
||||||
file.on("error", (err) => {
|
file.on("error", (err) => {
|
||||||
|
dlState.downloading = false;
|
||||||
|
dlState.progress = 0;
|
||||||
file.close();
|
file.close();
|
||||||
fs.unlinkSync(dest);
|
fs.unlinkSync(dest);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
}).on("error", (err) => {
|
}).on("error", (err) => {
|
||||||
|
dlState.downloading = false;
|
||||||
|
dlState.progress = 0;
|
||||||
file.close();
|
file.close();
|
||||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -84,27 +111,66 @@ function downloadFile(url, dest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIN_BINARY_SIZE = 1024 * 1024; // 1MB - cloudflared is ~30MB+
|
||||||
|
|
||||||
|
// Validate binary is executable on current platform and not truncated
|
||||||
|
function isValidBinary(filePath) {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.size < MIN_BINARY_SIZE) return false;
|
||||||
|
const fd = fs.openSync(filePath, "r");
|
||||||
|
const buf = Buffer.alloc(4);
|
||||||
|
fs.readSync(fd, buf, 0, 4, 0);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
const magic = buf.toString("hex");
|
||||||
|
if (IS_WINDOWS) return magic.startsWith("4d5a"); // PE (MZ)
|
||||||
|
if (os.platform() === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe");
|
||||||
|
return magic.startsWith("7f454c46"); // ELF (Linux)
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadPromise = null;
|
||||||
|
|
||||||
export async function ensureCloudflared() {
|
export async function ensureCloudflared() {
|
||||||
|
if (downloadPromise) return downloadPromise;
|
||||||
|
downloadPromise = _ensureCloudflared().finally(() => { downloadPromise = null; });
|
||||||
|
return downloadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _ensureCloudflared() {
|
||||||
if (!fs.existsSync(BIN_DIR)) {
|
if (!fs.existsSync(BIN_DIR)) {
|
||||||
fs.mkdirSync(BIN_DIR, { recursive: true });
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up incomplete downloads from previous runs
|
||||||
|
const tmpPath = `${BIN_PATH}.tmp`;
|
||||||
|
if (fs.existsSync(tmpPath)) {
|
||||||
|
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
if (fs.existsSync(BIN_PATH)) {
|
if (fs.existsSync(BIN_PATH)) {
|
||||||
if (!IS_WINDOWS) {
|
if (!isValidBinary(BIN_PATH)) {
|
||||||
fs.chmodSync(BIN_PATH, "755");
|
console.log("[cloudflared] Invalid binary detected, re-downloading...");
|
||||||
|
fs.unlinkSync(BIN_PATH);
|
||||||
|
} else {
|
||||||
|
if (!IS_WINDOWS) fs.chmodSync(BIN_PATH, "755");
|
||||||
|
return BIN_PATH;
|
||||||
}
|
}
|
||||||
return BIN_PATH;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getDownloadUrl();
|
const url = getDownloadUrl();
|
||||||
const isArchive = url.endsWith(".tgz");
|
const isArchive = url.endsWith(".tgz");
|
||||||
const downloadDest = isArchive ? path.join(BIN_DIR, "cloudflared.tgz") : BIN_PATH;
|
const downloadDest = isArchive ? path.join(BIN_DIR, "cloudflared.tgz.tmp") : tmpPath;
|
||||||
|
|
||||||
await downloadFile(url, downloadDest);
|
await downloadFile(url, downloadDest);
|
||||||
|
|
||||||
if (isArchive) {
|
if (isArchive) {
|
||||||
execSync(`tar -xzf "${downloadDest}" -C "${BIN_DIR}"`, { stdio: "pipe", windowsHide: true });
|
execSync(`tar -xzf "${downloadDest}" -C "${BIN_DIR}"`, { stdio: "pipe", windowsHide: true });
|
||||||
fs.unlinkSync(downloadDest);
|
fs.unlinkSync(downloadDest);
|
||||||
|
} else {
|
||||||
|
fs.renameSync(downloadDest, BIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IS_WINDOWS) {
|
if (!IS_WINDOWS) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||||
|
|
||||||
const TUNNEL_DIR = path.join(os.homedir(), ".9router", "tunnel");
|
const TUNNEL_DIR = path.join(DATA_DIR, "tunnel");
|
||||||
const STATE_FILE = path.join(TUNNEL_DIR, "state.json");
|
const STATE_FILE = path.join(TUNNEL_DIR, "state.json");
|
||||||
const CLOUDFLARED_PID_FILE = path.join(TUNNEL_DIR, "cloudflared.pid");
|
const CLOUDFLARED_PID_FILE = path.join(TUNNEL_DIR, "cloudflared.pid");
|
||||||
const TAILSCALE_PID_FILE = path.join(TUNNEL_DIR, "tailscale.pid");
|
const TAILSCALE_PID_FILE = path.join(TUNNEL_DIR, "tailscale.pid");
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import os from "os";
|
|||||||
import { execSync, spawn } from "child_process";
|
import { execSync, spawn } from "child_process";
|
||||||
import { execWithPassword } from "@/mitm/dns/dnsConfig";
|
import { execWithPassword } from "@/mitm/dns/dnsConfig";
|
||||||
import { saveTailscalePid, loadTailscalePid, clearTailscalePid } from "./state.js";
|
import { saveTailscalePid, loadTailscalePid, clearTailscalePid } from "./state.js";
|
||||||
|
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||||
|
|
||||||
const BIN_DIR = path.join(os.homedir(), ".9router", "bin");
|
const BIN_DIR = path.join(DATA_DIR, "bin");
|
||||||
const IS_MAC = os.platform() === "darwin";
|
const IS_MAC = os.platform() === "darwin";
|
||||||
const IS_LINUX = os.platform() === "linux";
|
const IS_LINUX = os.platform() === "linux";
|
||||||
const IS_WINDOWS = os.platform() === "win32";
|
const IS_WINDOWS = os.platform() === "win32";
|
||||||
const TAILSCALE_BIN = path.join(BIN_DIR, IS_WINDOWS ? "tailscale.exe" : "tailscale");
|
const TAILSCALE_BIN = path.join(BIN_DIR, IS_WINDOWS ? "tailscale.exe" : "tailscale");
|
||||||
|
|
||||||
// Custom socket for userspace-networking mode (no root required)
|
// Custom socket for userspace-networking mode (no root required)
|
||||||
const TAILSCALE_DIR = path.join(os.homedir(), ".9router", "tailscale");
|
const TAILSCALE_DIR = path.join(DATA_DIR, "tailscale");
|
||||||
export const TAILSCALE_SOCKET = path.join(TAILSCALE_DIR, "tailscaled.sock");
|
export const TAILSCALE_SOCKET = path.join(TAILSCALE_DIR, "tailscaled.sock");
|
||||||
const SOCKET_FLAG = IS_WINDOWS ? [] : ["--socket", TAILSCALE_SOCKET];
|
const SOCKET_FLAG = IS_WINDOWS ? [] : ["--socket", TAILSCALE_SOCKET];
|
||||||
|
|
||||||
@@ -281,7 +282,21 @@ async function installTailscaleWindows(log) {
|
|||||||
|
|
||||||
/** Start tailscaled with sudo (TUN mode required for Funnel) */
|
/** Start tailscaled with sudo (TUN mode required for Funnel) */
|
||||||
export async function startDaemonWithPassword(sudoPassword) {
|
export async function startDaemonWithPassword(sudoPassword) {
|
||||||
if (IS_WINDOWS) return;
|
if (IS_WINDOWS) {
|
||||||
|
// Windows: tailscale runs as a Windows Service, try to start it
|
||||||
|
try {
|
||||||
|
const bin = getTailscaleBin();
|
||||||
|
if (bin) {
|
||||||
|
execSync(`"${bin}" status --json`, { stdio: "ignore", windowsHide: true, timeout: 3000 });
|
||||||
|
return; // Already running
|
||||||
|
}
|
||||||
|
} catch { /* not running */ }
|
||||||
|
try {
|
||||||
|
execSync("net start Tailscale", { stdio: "ignore", windowsHide: true, timeout: 10000 });
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
} catch { /* may need admin, or already running */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if daemon already responds
|
// Check if daemon already responds
|
||||||
try {
|
try {
|
||||||
@@ -387,7 +402,7 @@ export function startLogin(hostname) {
|
|||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
const url = parseAuthUrl(output);
|
const url = parseAuthUrl(output);
|
||||||
if (url) resolve({ authUrl: url });
|
if (url) resolve({ authUrl: url });
|
||||||
else if (isTailscaleLoggedIn()) resolve({ alreadyLoggedIn: true });
|
else if (code === 0 || isTailscaleLoggedIn()) resolve({ alreadyLoggedIn: true });
|
||||||
else reject(new Error(`tailscale up exited with code ${code}`));
|
else reject(new Error(`tailscale up exited with code ${code}`));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-44
@@ -2,53 +2,10 @@ import { Low } from "lowdb";
|
|||||||
import { JSONFile } from "lowdb/node";
|
import { JSONFile } from "lowdb/node";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { fileURLToPath } from "url";
|
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||||
|
|
||||||
const isCloud = typeof caches !== 'undefined' || typeof caches === 'object';
|
const isCloud = typeof caches !== 'undefined' || typeof caches === 'object';
|
||||||
|
|
||||||
// Get app name from root package.json config
|
|
||||||
function getAppName() {
|
|
||||||
if (isCloud) return "9router"; // Skip file system access in Workers
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
// Look for root package.json (monorepo root)
|
|
||||||
const rootPkgPath = path.resolve(__dirname, "../../../package.json");
|
|
||||||
try {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
|
|
||||||
return pkg.config?.appName || "9router";
|
|
||||||
} catch {
|
|
||||||
return "9router";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user data directory based on platform
|
|
||||||
function getUserDataDir() {
|
|
||||||
if (isCloud) return "/tmp"; // Fallback for Workers
|
|
||||||
|
|
||||||
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const platform = process.platform;
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
const appName = getAppName();
|
|
||||||
|
|
||||||
if (platform === "win32") {
|
|
||||||
return path.join(process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), appName);
|
|
||||||
} else {
|
|
||||||
// macOS & Linux: ~/.{appName}
|
|
||||||
return path.join(homeDir, `.${appName}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[usageDb] Failed to get user data directory:", error.message);
|
|
||||||
// Fallback to cwd if homedir fails
|
|
||||||
return path.join(process.cwd(), ".9router");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data file path - stored in user home directory
|
|
||||||
const DATA_DIR = getUserDataDir();
|
|
||||||
const DB_FILE = isCloud ? null : path.join(DATA_DIR, "usage.json");
|
const DB_FILE = isCloud ? null : path.join(DATA_DIR, "usage.json");
|
||||||
const LOG_FILE = isCloud ? null : path.join(DATA_DIR, "log.txt");
|
const LOG_FILE = isCloud ? null : path.join(DATA_DIR, "log.txt");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user