@kmmao/happy-agent 0.3.1 → 0.3.2
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/index.cjs +147 -1
- package/dist/index.d.cts +29 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.mjs +148 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -18,7 +18,7 @@ var path = require('path');
|
|
|
18
18
|
var fs = require('fs');
|
|
19
19
|
var os = require('os');
|
|
20
20
|
|
|
21
|
-
var version = "0.3.
|
|
21
|
+
var version = "0.3.2";
|
|
22
22
|
|
|
23
23
|
function loadConfig() {
|
|
24
24
|
const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
|
|
@@ -1351,11 +1351,114 @@ class SessionClient extends node_events.EventEmitter {
|
|
|
1351
1351
|
}
|
|
1352
1352
|
}
|
|
1353
1353
|
|
|
1354
|
+
const NOT_INSTALLED = Object.freeze({ status: "not-installed" });
|
|
1355
|
+
const DISCONNECTED = Object.freeze({ status: "disconnected" });
|
|
1356
|
+
const DETECT_TIMEOUT_MS = 3e3;
|
|
1357
|
+
async function detectTailscale() {
|
|
1358
|
+
try {
|
|
1359
|
+
const raw = await execTailscale(["status", "--json"]);
|
|
1360
|
+
return parseTailscaleStatus(raw);
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
if (isNotFound(err)) {
|
|
1363
|
+
logger.debug("[TAILSCALE] tailscale binary not found");
|
|
1364
|
+
return NOT_INSTALLED;
|
|
1365
|
+
}
|
|
1366
|
+
logger.debug(`[TAILSCALE] detection failed: ${String(err)}`);
|
|
1367
|
+
return DISCONNECTED;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
async function detectTailscaleServe() {
|
|
1371
|
+
try {
|
|
1372
|
+
const raw = await execTailscale(["serve", "status", "--json"]);
|
|
1373
|
+
return parseTailscaleServeStatus(raw);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
logger.debug(`[TAILSCALE] serve detection failed: ${String(err)}`);
|
|
1376
|
+
return [];
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
function execTailscale(args) {
|
|
1380
|
+
return new Promise((resolve, reject) => {
|
|
1381
|
+
child_process.execFile("tailscale", args, { timeout: DETECT_TIMEOUT_MS }, (err, stdout) => {
|
|
1382
|
+
if (err) return reject(err);
|
|
1383
|
+
resolve(stdout);
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
function parseTailscaleStatus(raw) {
|
|
1388
|
+
let json;
|
|
1389
|
+
try {
|
|
1390
|
+
json = JSON.parse(raw);
|
|
1391
|
+
} catch {
|
|
1392
|
+
logger.debug("[TAILSCALE] failed to parse status JSON");
|
|
1393
|
+
return DISCONNECTED;
|
|
1394
|
+
}
|
|
1395
|
+
const backendState = json.BackendState;
|
|
1396
|
+
if (backendState && backendState !== "Running") {
|
|
1397
|
+
logger.debug(`[TAILSCALE] backend state: ${backendState}`);
|
|
1398
|
+
return DISCONNECTED;
|
|
1399
|
+
}
|
|
1400
|
+
const self = json.Self;
|
|
1401
|
+
if (!self) {
|
|
1402
|
+
logger.debug("[TAILSCALE] no Self node in status output");
|
|
1403
|
+
return DISCONNECTED;
|
|
1404
|
+
}
|
|
1405
|
+
const ips = self.TailscaleIPs ?? [];
|
|
1406
|
+
const ipv4 = ips.find((ip) => ip.includes("."));
|
|
1407
|
+
const ipv6 = ips.find((ip) => ip.includes(":"));
|
|
1408
|
+
const dnsName = self.DNSName;
|
|
1409
|
+
const hostname = dnsName?.replace(/\.$/, "").split(".")[0];
|
|
1410
|
+
const tailnetName = dnsName ? dnsName.replace(/\.$/, "").split(".").slice(1).join(".") : void 0;
|
|
1411
|
+
const version = json.Version;
|
|
1412
|
+
logger.debug(
|
|
1413
|
+
`[TAILSCALE] detected: ipv4=${ipv4 ?? "none"}, hostname=${hostname ?? "none"}`
|
|
1414
|
+
);
|
|
1415
|
+
return {
|
|
1416
|
+
status: "connected",
|
|
1417
|
+
ipv4,
|
|
1418
|
+
ipv6,
|
|
1419
|
+
hostname,
|
|
1420
|
+
tailnetName,
|
|
1421
|
+
version
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function parseTailscaleServeStatus(raw) {
|
|
1425
|
+
let json;
|
|
1426
|
+
try {
|
|
1427
|
+
json = JSON.parse(raw);
|
|
1428
|
+
} catch {
|
|
1429
|
+
logger.debug("[TAILSCALE] failed to parse serve status JSON");
|
|
1430
|
+
return [];
|
|
1431
|
+
}
|
|
1432
|
+
const web = json.Web ?? {};
|
|
1433
|
+
const allowFunnel = json.AllowFunnel ?? {};
|
|
1434
|
+
const entries = [];
|
|
1435
|
+
for (const [hostPort, config] of Object.entries(web)) {
|
|
1436
|
+
const colonIdx = hostPort.lastIndexOf(":");
|
|
1437
|
+
if (colonIdx === -1) continue;
|
|
1438
|
+
const hostname = hostPort.slice(0, colonIdx);
|
|
1439
|
+
const port = parseInt(hostPort.slice(colonIdx + 1), 10);
|
|
1440
|
+
if (!Number.isFinite(port)) continue;
|
|
1441
|
+
const handlers = config.Handlers ?? {};
|
|
1442
|
+
const rootHandler = handlers["/"];
|
|
1443
|
+
const target = rootHandler?.Proxy ?? "unknown";
|
|
1444
|
+
const funnel = allowFunnel[hostPort] === true;
|
|
1445
|
+
entries.push({ port, protocol: "HTTPS", target, funnel, hostname });
|
|
1446
|
+
}
|
|
1447
|
+
logger.debug(`[TAILSCALE] detected ${entries.length} serve entries`);
|
|
1448
|
+
return entries;
|
|
1449
|
+
}
|
|
1450
|
+
function isNotFound(err) {
|
|
1451
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const TAILSCALE_REFRESH_MS = 5 * 60 * 1e3;
|
|
1354
1455
|
class MachineClient {
|
|
1355
1456
|
machine;
|
|
1356
1457
|
rpcHandlerManager;
|
|
1357
1458
|
socket;
|
|
1358
1459
|
keepAliveInterval = null;
|
|
1460
|
+
tailscaleRefreshInterval = null;
|
|
1461
|
+
lastTailscaleInfo = null;
|
|
1359
1462
|
token;
|
|
1360
1463
|
serverUrl;
|
|
1361
1464
|
onEphemeral;
|
|
@@ -1393,12 +1496,21 @@ class MachineClient {
|
|
|
1393
1496
|
this.socket.on("connect", () => {
|
|
1394
1497
|
logger.debug("[MACHINE] Connected to server");
|
|
1395
1498
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
1499
|
+
this.updateDaemonState((state) => ({
|
|
1500
|
+
...state,
|
|
1501
|
+
status: "running",
|
|
1502
|
+
pid: process.pid,
|
|
1503
|
+
startedAt: Date.now(),
|
|
1504
|
+
tailscale: this.lastTailscaleInfo ?? state?.tailscale
|
|
1505
|
+
}));
|
|
1396
1506
|
this.startKeepAlive();
|
|
1507
|
+
this.startTailscaleRefresh();
|
|
1397
1508
|
});
|
|
1398
1509
|
this.socket.on("disconnect", () => {
|
|
1399
1510
|
logger.debug("[MACHINE] Disconnected from server");
|
|
1400
1511
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
1401
1512
|
this.stopKeepAlive();
|
|
1513
|
+
this.stopTailscaleRefresh();
|
|
1402
1514
|
});
|
|
1403
1515
|
this.socket.on(
|
|
1404
1516
|
"rpc-request",
|
|
@@ -1512,9 +1624,14 @@ class MachineClient {
|
|
|
1512
1624
|
// -----------------------------------------------------------------------
|
|
1513
1625
|
// Lifecycle
|
|
1514
1626
|
// -----------------------------------------------------------------------
|
|
1627
|
+
/** Seed initial Tailscale info detected before connect. */
|
|
1628
|
+
setTailscaleInfo(info) {
|
|
1629
|
+
this.lastTailscaleInfo = info;
|
|
1630
|
+
}
|
|
1515
1631
|
shutdown() {
|
|
1516
1632
|
logger.debug("[MACHINE] Shutting down");
|
|
1517
1633
|
this.stopKeepAlive();
|
|
1634
|
+
this.stopTailscaleRefresh();
|
|
1518
1635
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
1519
1636
|
this.socket?.close();
|
|
1520
1637
|
}
|
|
@@ -1536,6 +1653,35 @@ class MachineClient {
|
|
|
1536
1653
|
this.keepAliveInterval = null;
|
|
1537
1654
|
}
|
|
1538
1655
|
}
|
|
1656
|
+
startTailscaleRefresh() {
|
|
1657
|
+
this.stopTailscaleRefresh();
|
|
1658
|
+
this.tailscaleRefreshInterval = setInterval(async () => {
|
|
1659
|
+
const info = await detectTailscale();
|
|
1660
|
+
const serves = info.status === "connected" ? await detectTailscaleServe() : [];
|
|
1661
|
+
const fullInfo = { ...info, serves };
|
|
1662
|
+
if (tailscaleChanged(this.lastTailscaleInfo, fullInfo)) {
|
|
1663
|
+
logger.debug(
|
|
1664
|
+
`[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
|
|
1665
|
+
);
|
|
1666
|
+
this.lastTailscaleInfo = fullInfo;
|
|
1667
|
+
this.updateDaemonState((state) => {
|
|
1668
|
+
if (!state) return { status: "running", tailscale: fullInfo };
|
|
1669
|
+
return { ...state, tailscale: fullInfo };
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
}, TAILSCALE_REFRESH_MS);
|
|
1673
|
+
logger.debug("[MACHINE] Tailscale refresh started (5m interval)");
|
|
1674
|
+
}
|
|
1675
|
+
stopTailscaleRefresh() {
|
|
1676
|
+
if (this.tailscaleRefreshInterval) {
|
|
1677
|
+
clearInterval(this.tailscaleRefreshInterval);
|
|
1678
|
+
this.tailscaleRefreshInterval = null;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function tailscaleChanged(prev, next) {
|
|
1683
|
+
if (!prev) return true;
|
|
1684
|
+
return prev.status !== next.status || prev.ipv4 !== next.ipv4 || prev.ipv6 !== next.ipv6 || prev.hostname !== next.hostname || JSON.stringify(prev.serves) !== JSON.stringify(next.serves);
|
|
1539
1685
|
}
|
|
1540
1686
|
|
|
1541
1687
|
function formatTime(ts) {
|
package/dist/index.d.cts
CHANGED
|
@@ -298,6 +298,29 @@ declare class SessionClient extends EventEmitter {
|
|
|
298
298
|
private setupSocketListeners;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Tailscale detection utility for happy-agent.
|
|
303
|
+
*
|
|
304
|
+
* Adapted from happy-cli/src/utils/tailscale.ts — keep in sync.
|
|
305
|
+
*/
|
|
306
|
+
type TailscaleStatus = "connected" | "disconnected" | "not-installed";
|
|
307
|
+
type TailscaleServeEntry = {
|
|
308
|
+
port: number;
|
|
309
|
+
protocol: string;
|
|
310
|
+
target: string;
|
|
311
|
+
funnel: boolean;
|
|
312
|
+
hostname: string;
|
|
313
|
+
};
|
|
314
|
+
type TailscaleInfo = {
|
|
315
|
+
status: TailscaleStatus;
|
|
316
|
+
ipv4?: string;
|
|
317
|
+
ipv6?: string;
|
|
318
|
+
hostname?: string;
|
|
319
|
+
tailnetName?: string;
|
|
320
|
+
version?: string;
|
|
321
|
+
serves?: TailscaleServeEntry[];
|
|
322
|
+
};
|
|
323
|
+
|
|
301
324
|
/**
|
|
302
325
|
* Machine WebSocket client — trimmed from CLI's ApiMachineClient.
|
|
303
326
|
*
|
|
@@ -331,6 +354,8 @@ declare class MachineClient {
|
|
|
331
354
|
readonly rpcHandlerManager: RpcHandlerManager;
|
|
332
355
|
private socket;
|
|
333
356
|
private keepAliveInterval;
|
|
357
|
+
private tailscaleRefreshInterval;
|
|
358
|
+
private lastTailscaleInfo;
|
|
334
359
|
private readonly token;
|
|
335
360
|
private readonly serverUrl;
|
|
336
361
|
private readonly onEphemeral?;
|
|
@@ -338,9 +363,13 @@ declare class MachineClient {
|
|
|
338
363
|
connect(): void;
|
|
339
364
|
updateMachineMetadata(handler: (metadata: MachineMetadata | null) => MachineMetadata): Promise<void>;
|
|
340
365
|
updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
|
|
366
|
+
/** Seed initial Tailscale info detected before connect. */
|
|
367
|
+
setTailscaleInfo(info: TailscaleInfo): void;
|
|
341
368
|
shutdown(): void;
|
|
342
369
|
private startKeepAlive;
|
|
343
370
|
private stopKeepAlive;
|
|
371
|
+
private startTailscaleRefresh;
|
|
372
|
+
private stopTailscaleRefresh;
|
|
344
373
|
}
|
|
345
374
|
|
|
346
375
|
export { MachineClient, RpcHandlerManager, SessionClient, authLogin, authLogout, authStatus, createRpcHandlerManager, createSession, deleteSession, fetchMessagesAfterSeq, getOrCreateMachine, getSessionMessages, listActiveSessions, listMachines, listSessions, loadConfig, readCredentials, requireCredentials, resolveSessionEncryption, sendMessagesBatch };
|
package/dist/index.d.mts
CHANGED
|
@@ -298,6 +298,29 @@ declare class SessionClient extends EventEmitter {
|
|
|
298
298
|
private setupSocketListeners;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Tailscale detection utility for happy-agent.
|
|
303
|
+
*
|
|
304
|
+
* Adapted from happy-cli/src/utils/tailscale.ts — keep in sync.
|
|
305
|
+
*/
|
|
306
|
+
type TailscaleStatus = "connected" | "disconnected" | "not-installed";
|
|
307
|
+
type TailscaleServeEntry = {
|
|
308
|
+
port: number;
|
|
309
|
+
protocol: string;
|
|
310
|
+
target: string;
|
|
311
|
+
funnel: boolean;
|
|
312
|
+
hostname: string;
|
|
313
|
+
};
|
|
314
|
+
type TailscaleInfo = {
|
|
315
|
+
status: TailscaleStatus;
|
|
316
|
+
ipv4?: string;
|
|
317
|
+
ipv6?: string;
|
|
318
|
+
hostname?: string;
|
|
319
|
+
tailnetName?: string;
|
|
320
|
+
version?: string;
|
|
321
|
+
serves?: TailscaleServeEntry[];
|
|
322
|
+
};
|
|
323
|
+
|
|
301
324
|
/**
|
|
302
325
|
* Machine WebSocket client — trimmed from CLI's ApiMachineClient.
|
|
303
326
|
*
|
|
@@ -331,6 +354,8 @@ declare class MachineClient {
|
|
|
331
354
|
readonly rpcHandlerManager: RpcHandlerManager;
|
|
332
355
|
private socket;
|
|
333
356
|
private keepAliveInterval;
|
|
357
|
+
private tailscaleRefreshInterval;
|
|
358
|
+
private lastTailscaleInfo;
|
|
334
359
|
private readonly token;
|
|
335
360
|
private readonly serverUrl;
|
|
336
361
|
private readonly onEphemeral?;
|
|
@@ -338,9 +363,13 @@ declare class MachineClient {
|
|
|
338
363
|
connect(): void;
|
|
339
364
|
updateMachineMetadata(handler: (metadata: MachineMetadata | null) => MachineMetadata): Promise<void>;
|
|
340
365
|
updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
|
|
366
|
+
/** Seed initial Tailscale info detected before connect. */
|
|
367
|
+
setTailscaleInfo(info: TailscaleInfo): void;
|
|
341
368
|
shutdown(): void;
|
|
342
369
|
private startKeepAlive;
|
|
343
370
|
private stopKeepAlive;
|
|
371
|
+
private startTailscaleRefresh;
|
|
372
|
+
private stopTailscaleRefresh;
|
|
344
373
|
}
|
|
345
374
|
|
|
346
375
|
export { MachineClient, RpcHandlerManager, SessionClient, authLogin, authLogout, authStatus, createRpcHandlerManager, createSession, deleteSession, fetchMessagesAfterSeq, getOrCreateMachine, getSessionMessages, listActiveSessions, listMachines, listSessions, loadConfig, readCredentials, requireCredentials, resolveSessionEncryption, sendMessagesBatch };
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import axios, { AxiosError } from 'axios';
|
|
|
8
8
|
import qrcode from 'qrcode-terminal';
|
|
9
9
|
import { EventEmitter } from 'node:events';
|
|
10
10
|
import { io } from 'socket.io-client';
|
|
11
|
-
import { exec } from 'child_process';
|
|
11
|
+
import { exec, execFile } from 'child_process';
|
|
12
12
|
import { promisify } from 'util';
|
|
13
13
|
import { readFile, mkdir, writeFile, readdir, stat } from 'fs/promises';
|
|
14
14
|
import { createHash as createHash$1 } from 'crypto';
|
|
@@ -16,7 +16,7 @@ import { join as join$1, resolve } from 'path';
|
|
|
16
16
|
import { realpathSync } from 'fs';
|
|
17
17
|
import { tmpdir } from 'os';
|
|
18
18
|
|
|
19
|
-
var version = "0.3.
|
|
19
|
+
var version = "0.3.2";
|
|
20
20
|
|
|
21
21
|
function loadConfig() {
|
|
22
22
|
const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
|
|
@@ -1349,11 +1349,114 @@ class SessionClient extends EventEmitter {
|
|
|
1349
1349
|
}
|
|
1350
1350
|
}
|
|
1351
1351
|
|
|
1352
|
+
const NOT_INSTALLED = Object.freeze({ status: "not-installed" });
|
|
1353
|
+
const DISCONNECTED = Object.freeze({ status: "disconnected" });
|
|
1354
|
+
const DETECT_TIMEOUT_MS = 3e3;
|
|
1355
|
+
async function detectTailscale() {
|
|
1356
|
+
try {
|
|
1357
|
+
const raw = await execTailscale(["status", "--json"]);
|
|
1358
|
+
return parseTailscaleStatus(raw);
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
if (isNotFound(err)) {
|
|
1361
|
+
logger.debug("[TAILSCALE] tailscale binary not found");
|
|
1362
|
+
return NOT_INSTALLED;
|
|
1363
|
+
}
|
|
1364
|
+
logger.debug(`[TAILSCALE] detection failed: ${String(err)}`);
|
|
1365
|
+
return DISCONNECTED;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async function detectTailscaleServe() {
|
|
1369
|
+
try {
|
|
1370
|
+
const raw = await execTailscale(["serve", "status", "--json"]);
|
|
1371
|
+
return parseTailscaleServeStatus(raw);
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
logger.debug(`[TAILSCALE] serve detection failed: ${String(err)}`);
|
|
1374
|
+
return [];
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function execTailscale(args) {
|
|
1378
|
+
return new Promise((resolve, reject) => {
|
|
1379
|
+
execFile("tailscale", args, { timeout: DETECT_TIMEOUT_MS }, (err, stdout) => {
|
|
1380
|
+
if (err) return reject(err);
|
|
1381
|
+
resolve(stdout);
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
function parseTailscaleStatus(raw) {
|
|
1386
|
+
let json;
|
|
1387
|
+
try {
|
|
1388
|
+
json = JSON.parse(raw);
|
|
1389
|
+
} catch {
|
|
1390
|
+
logger.debug("[TAILSCALE] failed to parse status JSON");
|
|
1391
|
+
return DISCONNECTED;
|
|
1392
|
+
}
|
|
1393
|
+
const backendState = json.BackendState;
|
|
1394
|
+
if (backendState && backendState !== "Running") {
|
|
1395
|
+
logger.debug(`[TAILSCALE] backend state: ${backendState}`);
|
|
1396
|
+
return DISCONNECTED;
|
|
1397
|
+
}
|
|
1398
|
+
const self = json.Self;
|
|
1399
|
+
if (!self) {
|
|
1400
|
+
logger.debug("[TAILSCALE] no Self node in status output");
|
|
1401
|
+
return DISCONNECTED;
|
|
1402
|
+
}
|
|
1403
|
+
const ips = self.TailscaleIPs ?? [];
|
|
1404
|
+
const ipv4 = ips.find((ip) => ip.includes("."));
|
|
1405
|
+
const ipv6 = ips.find((ip) => ip.includes(":"));
|
|
1406
|
+
const dnsName = self.DNSName;
|
|
1407
|
+
const hostname = dnsName?.replace(/\.$/, "").split(".")[0];
|
|
1408
|
+
const tailnetName = dnsName ? dnsName.replace(/\.$/, "").split(".").slice(1).join(".") : void 0;
|
|
1409
|
+
const version = json.Version;
|
|
1410
|
+
logger.debug(
|
|
1411
|
+
`[TAILSCALE] detected: ipv4=${ipv4 ?? "none"}, hostname=${hostname ?? "none"}`
|
|
1412
|
+
);
|
|
1413
|
+
return {
|
|
1414
|
+
status: "connected",
|
|
1415
|
+
ipv4,
|
|
1416
|
+
ipv6,
|
|
1417
|
+
hostname,
|
|
1418
|
+
tailnetName,
|
|
1419
|
+
version
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function parseTailscaleServeStatus(raw) {
|
|
1423
|
+
let json;
|
|
1424
|
+
try {
|
|
1425
|
+
json = JSON.parse(raw);
|
|
1426
|
+
} catch {
|
|
1427
|
+
logger.debug("[TAILSCALE] failed to parse serve status JSON");
|
|
1428
|
+
return [];
|
|
1429
|
+
}
|
|
1430
|
+
const web = json.Web ?? {};
|
|
1431
|
+
const allowFunnel = json.AllowFunnel ?? {};
|
|
1432
|
+
const entries = [];
|
|
1433
|
+
for (const [hostPort, config] of Object.entries(web)) {
|
|
1434
|
+
const colonIdx = hostPort.lastIndexOf(":");
|
|
1435
|
+
if (colonIdx === -1) continue;
|
|
1436
|
+
const hostname = hostPort.slice(0, colonIdx);
|
|
1437
|
+
const port = parseInt(hostPort.slice(colonIdx + 1), 10);
|
|
1438
|
+
if (!Number.isFinite(port)) continue;
|
|
1439
|
+
const handlers = config.Handlers ?? {};
|
|
1440
|
+
const rootHandler = handlers["/"];
|
|
1441
|
+
const target = rootHandler?.Proxy ?? "unknown";
|
|
1442
|
+
const funnel = allowFunnel[hostPort] === true;
|
|
1443
|
+
entries.push({ port, protocol: "HTTPS", target, funnel, hostname });
|
|
1444
|
+
}
|
|
1445
|
+
logger.debug(`[TAILSCALE] detected ${entries.length} serve entries`);
|
|
1446
|
+
return entries;
|
|
1447
|
+
}
|
|
1448
|
+
function isNotFound(err) {
|
|
1449
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const TAILSCALE_REFRESH_MS = 5 * 60 * 1e3;
|
|
1352
1453
|
class MachineClient {
|
|
1353
1454
|
machine;
|
|
1354
1455
|
rpcHandlerManager;
|
|
1355
1456
|
socket;
|
|
1356
1457
|
keepAliveInterval = null;
|
|
1458
|
+
tailscaleRefreshInterval = null;
|
|
1459
|
+
lastTailscaleInfo = null;
|
|
1357
1460
|
token;
|
|
1358
1461
|
serverUrl;
|
|
1359
1462
|
onEphemeral;
|
|
@@ -1391,12 +1494,21 @@ class MachineClient {
|
|
|
1391
1494
|
this.socket.on("connect", () => {
|
|
1392
1495
|
logger.debug("[MACHINE] Connected to server");
|
|
1393
1496
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
1497
|
+
this.updateDaemonState((state) => ({
|
|
1498
|
+
...state,
|
|
1499
|
+
status: "running",
|
|
1500
|
+
pid: process.pid,
|
|
1501
|
+
startedAt: Date.now(),
|
|
1502
|
+
tailscale: this.lastTailscaleInfo ?? state?.tailscale
|
|
1503
|
+
}));
|
|
1394
1504
|
this.startKeepAlive();
|
|
1505
|
+
this.startTailscaleRefresh();
|
|
1395
1506
|
});
|
|
1396
1507
|
this.socket.on("disconnect", () => {
|
|
1397
1508
|
logger.debug("[MACHINE] Disconnected from server");
|
|
1398
1509
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
1399
1510
|
this.stopKeepAlive();
|
|
1511
|
+
this.stopTailscaleRefresh();
|
|
1400
1512
|
});
|
|
1401
1513
|
this.socket.on(
|
|
1402
1514
|
"rpc-request",
|
|
@@ -1510,9 +1622,14 @@ class MachineClient {
|
|
|
1510
1622
|
// -----------------------------------------------------------------------
|
|
1511
1623
|
// Lifecycle
|
|
1512
1624
|
// -----------------------------------------------------------------------
|
|
1625
|
+
/** Seed initial Tailscale info detected before connect. */
|
|
1626
|
+
setTailscaleInfo(info) {
|
|
1627
|
+
this.lastTailscaleInfo = info;
|
|
1628
|
+
}
|
|
1513
1629
|
shutdown() {
|
|
1514
1630
|
logger.debug("[MACHINE] Shutting down");
|
|
1515
1631
|
this.stopKeepAlive();
|
|
1632
|
+
this.stopTailscaleRefresh();
|
|
1516
1633
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
1517
1634
|
this.socket?.close();
|
|
1518
1635
|
}
|
|
@@ -1534,6 +1651,35 @@ class MachineClient {
|
|
|
1534
1651
|
this.keepAliveInterval = null;
|
|
1535
1652
|
}
|
|
1536
1653
|
}
|
|
1654
|
+
startTailscaleRefresh() {
|
|
1655
|
+
this.stopTailscaleRefresh();
|
|
1656
|
+
this.tailscaleRefreshInterval = setInterval(async () => {
|
|
1657
|
+
const info = await detectTailscale();
|
|
1658
|
+
const serves = info.status === "connected" ? await detectTailscaleServe() : [];
|
|
1659
|
+
const fullInfo = { ...info, serves };
|
|
1660
|
+
if (tailscaleChanged(this.lastTailscaleInfo, fullInfo)) {
|
|
1661
|
+
logger.debug(
|
|
1662
|
+
`[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
|
|
1663
|
+
);
|
|
1664
|
+
this.lastTailscaleInfo = fullInfo;
|
|
1665
|
+
this.updateDaemonState((state) => {
|
|
1666
|
+
if (!state) return { status: "running", tailscale: fullInfo };
|
|
1667
|
+
return { ...state, tailscale: fullInfo };
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
}, TAILSCALE_REFRESH_MS);
|
|
1671
|
+
logger.debug("[MACHINE] Tailscale refresh started (5m interval)");
|
|
1672
|
+
}
|
|
1673
|
+
stopTailscaleRefresh() {
|
|
1674
|
+
if (this.tailscaleRefreshInterval) {
|
|
1675
|
+
clearInterval(this.tailscaleRefreshInterval);
|
|
1676
|
+
this.tailscaleRefreshInterval = null;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function tailscaleChanged(prev, next) {
|
|
1681
|
+
if (!prev) return true;
|
|
1682
|
+
return prev.status !== next.status || prev.ipv4 !== next.ipv4 || prev.ipv6 !== next.ipv6 || prev.hostname !== next.hostname || JSON.stringify(prev.serves) !== JSON.stringify(next.serves);
|
|
1537
1683
|
}
|
|
1538
1684
|
|
|
1539
1685
|
function formatTime(ts) {
|