@kmmao/happy-agent 0.3.7 → 0.3.9
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 +42 -6
- package/dist/index.d.cts +72 -2
- package/dist/index.d.mts +72 -2
- package/dist/index.mjs +42 -6
- 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.9";
|
|
22
22
|
|
|
23
23
|
function loadConfig() {
|
|
24
24
|
const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
|
|
@@ -622,6 +622,7 @@ class RpcHandlerManager {
|
|
|
622
622
|
logger;
|
|
623
623
|
socket = null;
|
|
624
624
|
reregisterInterval = null;
|
|
625
|
+
fastRetryTimer = null;
|
|
625
626
|
constructor(config) {
|
|
626
627
|
this.scopePrefix = config.scopePrefix;
|
|
627
628
|
this.encryptionKey = config.encryptionKey;
|
|
@@ -681,11 +682,13 @@ class RpcHandlerManager {
|
|
|
681
682
|
onSocketConnect(socket) {
|
|
682
683
|
this.socket = socket;
|
|
683
684
|
this.registerAllHandlers(socket);
|
|
685
|
+
this.scheduleFastRetry(socket);
|
|
684
686
|
this.startReregisterInterval();
|
|
685
687
|
}
|
|
686
688
|
onSocketDisconnect() {
|
|
687
689
|
this.socket = null;
|
|
688
690
|
this.stopReregisterInterval();
|
|
691
|
+
this.cancelFastRetry();
|
|
689
692
|
}
|
|
690
693
|
getHandlerCount() {
|
|
691
694
|
return this.handlers.size;
|
|
@@ -741,7 +744,27 @@ class RpcHandlerManager {
|
|
|
741
744
|
}
|
|
742
745
|
}
|
|
743
746
|
/**
|
|
744
|
-
*
|
|
747
|
+
* Fast retry: 5 seconds after initial connect, re-register all handlers once.
|
|
748
|
+
* Covers the case where the first batch of registrations failed silently.
|
|
749
|
+
*/
|
|
750
|
+
scheduleFastRetry(socket) {
|
|
751
|
+
this.cancelFastRetry();
|
|
752
|
+
this.fastRetryTimer = setTimeout(() => {
|
|
753
|
+
this.fastRetryTimer = null;
|
|
754
|
+
if (this.socket === socket && this.handlers.size > 0) {
|
|
755
|
+
this.logger("[RPC] Fast retry: re-registering all handlers");
|
|
756
|
+
this.registerAllHandlers(socket);
|
|
757
|
+
}
|
|
758
|
+
}, 5e3);
|
|
759
|
+
}
|
|
760
|
+
cancelFastRetry() {
|
|
761
|
+
if (this.fastRetryTimer) {
|
|
762
|
+
clearTimeout(this.fastRetryTimer);
|
|
763
|
+
this.fastRetryTimer = null;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Periodic re-registration every 30s as a safety net.
|
|
745
768
|
*/
|
|
746
769
|
startReregisterInterval() {
|
|
747
770
|
this.stopReregisterInterval();
|
|
@@ -749,7 +772,7 @@ class RpcHandlerManager {
|
|
|
749
772
|
if (this.socket && this.handlers.size > 0) {
|
|
750
773
|
this.registerAllHandlers(this.socket);
|
|
751
774
|
}
|
|
752
|
-
},
|
|
775
|
+
}, 3e4);
|
|
753
776
|
}
|
|
754
777
|
stopReregisterInterval() {
|
|
755
778
|
if (this.reregisterInterval) {
|
|
@@ -1460,6 +1483,7 @@ class MachineClient {
|
|
|
1460
1483
|
keepAliveInterval = null;
|
|
1461
1484
|
tailscaleRefreshInterval = null;
|
|
1462
1485
|
lastTailscaleInfo = null;
|
|
1486
|
+
tunnelManager = null;
|
|
1463
1487
|
token;
|
|
1464
1488
|
serverUrl;
|
|
1465
1489
|
onEphemeral;
|
|
@@ -1629,6 +1653,10 @@ class MachineClient {
|
|
|
1629
1653
|
setTailscaleInfo(info) {
|
|
1630
1654
|
this.lastTailscaleInfo = info;
|
|
1631
1655
|
}
|
|
1656
|
+
/** Attach a TunnelManager for periodic tunnel state refresh. */
|
|
1657
|
+
setTunnelManager(manager) {
|
|
1658
|
+
this.tunnelManager = manager;
|
|
1659
|
+
}
|
|
1632
1660
|
shutdown() {
|
|
1633
1661
|
logger.debug("[MACHINE] Shutting down");
|
|
1634
1662
|
this.stopKeepAlive();
|
|
@@ -1660,14 +1688,22 @@ class MachineClient {
|
|
|
1660
1688
|
const info = await detectTailscale();
|
|
1661
1689
|
const serves = info.status === "connected" ? await detectTailscaleServe() : [];
|
|
1662
1690
|
const fullInfo = { ...info, serves };
|
|
1663
|
-
|
|
1691
|
+
const tsChanged = tailscaleChanged(this.lastTailscaleInfo, fullInfo);
|
|
1692
|
+
if (tsChanged) {
|
|
1664
1693
|
logger.debug(
|
|
1665
1694
|
`[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
|
|
1666
1695
|
);
|
|
1667
1696
|
this.lastTailscaleInfo = fullInfo;
|
|
1697
|
+
}
|
|
1698
|
+
const tunnels = this.tunnelManager ? await this.tunnelManager.detectAll() : void 0;
|
|
1699
|
+
if (tsChanged || tunnels) {
|
|
1668
1700
|
this.updateDaemonState((state) => {
|
|
1669
|
-
if (!state) return { status: "running", tailscale: fullInfo };
|
|
1670
|
-
return {
|
|
1701
|
+
if (!state) return { status: "running", tailscale: fullInfo, tunnels };
|
|
1702
|
+
return {
|
|
1703
|
+
...state,
|
|
1704
|
+
...tsChanged ? { tailscale: fullInfo } : {},
|
|
1705
|
+
...tunnels ? { tunnels } : {}
|
|
1706
|
+
};
|
|
1671
1707
|
});
|
|
1672
1708
|
}
|
|
1673
1709
|
}, TAILSCALE_REFRESH_MS);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { MachineMetadata, DaemonState } from '@kmmao/happy-wire';
|
|
2
|
+
import { MachineMetadata, DaemonState, TunnelProviderInfo, TunnelEntry, TunnelState } from '@kmmao/happy-wire';
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { Socket } from 'socket.io-client';
|
|
5
5
|
|
|
@@ -175,6 +175,7 @@ declare class RpcHandlerManager {
|
|
|
175
175
|
private readonly logger;
|
|
176
176
|
private socket;
|
|
177
177
|
private reregisterInterval;
|
|
178
|
+
private fastRetryTimer;
|
|
178
179
|
constructor(config: RpcHandlerConfig);
|
|
179
180
|
/**
|
|
180
181
|
* Register an RPC handler for a specific method
|
|
@@ -196,7 +197,13 @@ declare class RpcHandlerManager {
|
|
|
196
197
|
private emitRegisterWithRetry;
|
|
197
198
|
private registerAllHandlers;
|
|
198
199
|
/**
|
|
199
|
-
*
|
|
200
|
+
* Fast retry: 5 seconds after initial connect, re-register all handlers once.
|
|
201
|
+
* Covers the case where the first batch of registrations failed silently.
|
|
202
|
+
*/
|
|
203
|
+
private scheduleFastRetry;
|
|
204
|
+
private cancelFastRetry;
|
|
205
|
+
/**
|
|
206
|
+
* Periodic re-registration every 30s as a safety net.
|
|
200
207
|
*/
|
|
201
208
|
private startReregisterInterval;
|
|
202
209
|
private stopReregisterInterval;
|
|
@@ -278,6 +285,66 @@ type TailscaleInfo = {
|
|
|
278
285
|
serves?: TailscaleServeEntry[];
|
|
279
286
|
};
|
|
280
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Tunnel Provider abstraction — unified interface for all tunnel backends.
|
|
290
|
+
*
|
|
291
|
+
* Each provider (Tailscale, UPnP, Cloudflare, FRP, etc.) implements this
|
|
292
|
+
* interface. TunnelManager aggregates all providers and exposes a single API.
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
interface TunnelProvider {
|
|
296
|
+
/** Unique provider name: "tailscale" | "upnp" | "cloudflare" | "frp" */
|
|
297
|
+
readonly name: string;
|
|
298
|
+
/** Detect provider availability and list current tunnel entries. Never throws. */
|
|
299
|
+
detect(): Promise<TunnelProviderInfo>;
|
|
300
|
+
/** Add a tunnel mapping */
|
|
301
|
+
add(params: TunnelAddParams): Promise<TunnelOpResult>;
|
|
302
|
+
/** Remove a tunnel mapping */
|
|
303
|
+
remove(params: TunnelRemoveParams): Promise<TunnelOpResult>;
|
|
304
|
+
/** Toggle public/private access (not all providers support this) */
|
|
305
|
+
toggleAccess?(entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
|
|
306
|
+
}
|
|
307
|
+
interface TunnelAddParams {
|
|
308
|
+
localPort: number;
|
|
309
|
+
remotePort?: number;
|
|
310
|
+
protocol?: string;
|
|
311
|
+
path?: string;
|
|
312
|
+
publicAccess?: boolean;
|
|
313
|
+
hostname?: string;
|
|
314
|
+
}
|
|
315
|
+
interface TunnelRemoveParams {
|
|
316
|
+
localPort?: number;
|
|
317
|
+
remotePort?: number;
|
|
318
|
+
path?: string;
|
|
319
|
+
hostname?: string;
|
|
320
|
+
/** Remove entire site (all routes for this hostname) */
|
|
321
|
+
removeEntireSite?: boolean;
|
|
322
|
+
}
|
|
323
|
+
interface TunnelOpResult {
|
|
324
|
+
success: boolean;
|
|
325
|
+
error?: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* TunnelManager for happy-agent — aggregates all tunnel providers.
|
|
330
|
+
*/
|
|
331
|
+
|
|
332
|
+
declare class TunnelManager {
|
|
333
|
+
private readonly providers;
|
|
334
|
+
private refreshTimer;
|
|
335
|
+
private leaseRenewalTimer;
|
|
336
|
+
private lastState;
|
|
337
|
+
constructor(providers: TunnelProvider[]);
|
|
338
|
+
detectAll(): Promise<TunnelState>;
|
|
339
|
+
getLastState(): TunnelState;
|
|
340
|
+
getProvider(name: string): TunnelProvider | undefined;
|
|
341
|
+
add(providerName: string, params: TunnelAddParams): Promise<TunnelOpResult>;
|
|
342
|
+
remove(providerName: string, params: TunnelRemoveParams): Promise<TunnelOpResult>;
|
|
343
|
+
toggleAccess(providerName: string, entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
|
|
344
|
+
startRefresh(onChange: (state: TunnelState) => void, intervalMs?: number): void;
|
|
345
|
+
stopRefresh(): void;
|
|
346
|
+
}
|
|
347
|
+
|
|
281
348
|
/**
|
|
282
349
|
* Machine WebSocket client — trimmed from CLI's ApiMachineClient.
|
|
283
350
|
*
|
|
@@ -313,6 +380,7 @@ declare class MachineClient {
|
|
|
313
380
|
private keepAliveInterval;
|
|
314
381
|
private tailscaleRefreshInterval;
|
|
315
382
|
private lastTailscaleInfo;
|
|
383
|
+
private tunnelManager;
|
|
316
384
|
private readonly token;
|
|
317
385
|
private readonly serverUrl;
|
|
318
386
|
private readonly onEphemeral?;
|
|
@@ -322,6 +390,8 @@ declare class MachineClient {
|
|
|
322
390
|
updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
|
|
323
391
|
/** Seed initial Tailscale info detected before connect. */
|
|
324
392
|
setTailscaleInfo(info: TailscaleInfo): void;
|
|
393
|
+
/** Attach a TunnelManager for periodic tunnel state refresh. */
|
|
394
|
+
setTunnelManager(manager: TunnelManager): void;
|
|
325
395
|
shutdown(): void;
|
|
326
396
|
private startKeepAlive;
|
|
327
397
|
private stopKeepAlive;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { MachineMetadata, DaemonState } from '@kmmao/happy-wire';
|
|
2
|
+
import { MachineMetadata, DaemonState, TunnelProviderInfo, TunnelEntry, TunnelState } from '@kmmao/happy-wire';
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { Socket } from 'socket.io-client';
|
|
5
5
|
|
|
@@ -175,6 +175,7 @@ declare class RpcHandlerManager {
|
|
|
175
175
|
private readonly logger;
|
|
176
176
|
private socket;
|
|
177
177
|
private reregisterInterval;
|
|
178
|
+
private fastRetryTimer;
|
|
178
179
|
constructor(config: RpcHandlerConfig);
|
|
179
180
|
/**
|
|
180
181
|
* Register an RPC handler for a specific method
|
|
@@ -196,7 +197,13 @@ declare class RpcHandlerManager {
|
|
|
196
197
|
private emitRegisterWithRetry;
|
|
197
198
|
private registerAllHandlers;
|
|
198
199
|
/**
|
|
199
|
-
*
|
|
200
|
+
* Fast retry: 5 seconds after initial connect, re-register all handlers once.
|
|
201
|
+
* Covers the case where the first batch of registrations failed silently.
|
|
202
|
+
*/
|
|
203
|
+
private scheduleFastRetry;
|
|
204
|
+
private cancelFastRetry;
|
|
205
|
+
/**
|
|
206
|
+
* Periodic re-registration every 30s as a safety net.
|
|
200
207
|
*/
|
|
201
208
|
private startReregisterInterval;
|
|
202
209
|
private stopReregisterInterval;
|
|
@@ -278,6 +285,66 @@ type TailscaleInfo = {
|
|
|
278
285
|
serves?: TailscaleServeEntry[];
|
|
279
286
|
};
|
|
280
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Tunnel Provider abstraction — unified interface for all tunnel backends.
|
|
290
|
+
*
|
|
291
|
+
* Each provider (Tailscale, UPnP, Cloudflare, FRP, etc.) implements this
|
|
292
|
+
* interface. TunnelManager aggregates all providers and exposes a single API.
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
interface TunnelProvider {
|
|
296
|
+
/** Unique provider name: "tailscale" | "upnp" | "cloudflare" | "frp" */
|
|
297
|
+
readonly name: string;
|
|
298
|
+
/** Detect provider availability and list current tunnel entries. Never throws. */
|
|
299
|
+
detect(): Promise<TunnelProviderInfo>;
|
|
300
|
+
/** Add a tunnel mapping */
|
|
301
|
+
add(params: TunnelAddParams): Promise<TunnelOpResult>;
|
|
302
|
+
/** Remove a tunnel mapping */
|
|
303
|
+
remove(params: TunnelRemoveParams): Promise<TunnelOpResult>;
|
|
304
|
+
/** Toggle public/private access (not all providers support this) */
|
|
305
|
+
toggleAccess?(entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
|
|
306
|
+
}
|
|
307
|
+
interface TunnelAddParams {
|
|
308
|
+
localPort: number;
|
|
309
|
+
remotePort?: number;
|
|
310
|
+
protocol?: string;
|
|
311
|
+
path?: string;
|
|
312
|
+
publicAccess?: boolean;
|
|
313
|
+
hostname?: string;
|
|
314
|
+
}
|
|
315
|
+
interface TunnelRemoveParams {
|
|
316
|
+
localPort?: number;
|
|
317
|
+
remotePort?: number;
|
|
318
|
+
path?: string;
|
|
319
|
+
hostname?: string;
|
|
320
|
+
/** Remove entire site (all routes for this hostname) */
|
|
321
|
+
removeEntireSite?: boolean;
|
|
322
|
+
}
|
|
323
|
+
interface TunnelOpResult {
|
|
324
|
+
success: boolean;
|
|
325
|
+
error?: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* TunnelManager for happy-agent — aggregates all tunnel providers.
|
|
330
|
+
*/
|
|
331
|
+
|
|
332
|
+
declare class TunnelManager {
|
|
333
|
+
private readonly providers;
|
|
334
|
+
private refreshTimer;
|
|
335
|
+
private leaseRenewalTimer;
|
|
336
|
+
private lastState;
|
|
337
|
+
constructor(providers: TunnelProvider[]);
|
|
338
|
+
detectAll(): Promise<TunnelState>;
|
|
339
|
+
getLastState(): TunnelState;
|
|
340
|
+
getProvider(name: string): TunnelProvider | undefined;
|
|
341
|
+
add(providerName: string, params: TunnelAddParams): Promise<TunnelOpResult>;
|
|
342
|
+
remove(providerName: string, params: TunnelRemoveParams): Promise<TunnelOpResult>;
|
|
343
|
+
toggleAccess(providerName: string, entry: TunnelEntry, publicAccess: boolean): Promise<TunnelOpResult>;
|
|
344
|
+
startRefresh(onChange: (state: TunnelState) => void, intervalMs?: number): void;
|
|
345
|
+
stopRefresh(): void;
|
|
346
|
+
}
|
|
347
|
+
|
|
281
348
|
/**
|
|
282
349
|
* Machine WebSocket client — trimmed from CLI's ApiMachineClient.
|
|
283
350
|
*
|
|
@@ -313,6 +380,7 @@ declare class MachineClient {
|
|
|
313
380
|
private keepAliveInterval;
|
|
314
381
|
private tailscaleRefreshInterval;
|
|
315
382
|
private lastTailscaleInfo;
|
|
383
|
+
private tunnelManager;
|
|
316
384
|
private readonly token;
|
|
317
385
|
private readonly serverUrl;
|
|
318
386
|
private readonly onEphemeral?;
|
|
@@ -322,6 +390,8 @@ declare class MachineClient {
|
|
|
322
390
|
updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
|
|
323
391
|
/** Seed initial Tailscale info detected before connect. */
|
|
324
392
|
setTailscaleInfo(info: TailscaleInfo): void;
|
|
393
|
+
/** Attach a TunnelManager for periodic tunnel state refresh. */
|
|
394
|
+
setTunnelManager(manager: TunnelManager): void;
|
|
325
395
|
shutdown(): void;
|
|
326
396
|
private startKeepAlive;
|
|
327
397
|
private stopKeepAlive;
|
package/dist/index.mjs
CHANGED
|
@@ -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.9";
|
|
20
20
|
|
|
21
21
|
function loadConfig() {
|
|
22
22
|
const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
|
|
@@ -620,6 +620,7 @@ class RpcHandlerManager {
|
|
|
620
620
|
logger;
|
|
621
621
|
socket = null;
|
|
622
622
|
reregisterInterval = null;
|
|
623
|
+
fastRetryTimer = null;
|
|
623
624
|
constructor(config) {
|
|
624
625
|
this.scopePrefix = config.scopePrefix;
|
|
625
626
|
this.encryptionKey = config.encryptionKey;
|
|
@@ -679,11 +680,13 @@ class RpcHandlerManager {
|
|
|
679
680
|
onSocketConnect(socket) {
|
|
680
681
|
this.socket = socket;
|
|
681
682
|
this.registerAllHandlers(socket);
|
|
683
|
+
this.scheduleFastRetry(socket);
|
|
682
684
|
this.startReregisterInterval();
|
|
683
685
|
}
|
|
684
686
|
onSocketDisconnect() {
|
|
685
687
|
this.socket = null;
|
|
686
688
|
this.stopReregisterInterval();
|
|
689
|
+
this.cancelFastRetry();
|
|
687
690
|
}
|
|
688
691
|
getHandlerCount() {
|
|
689
692
|
return this.handlers.size;
|
|
@@ -739,7 +742,27 @@ class RpcHandlerManager {
|
|
|
739
742
|
}
|
|
740
743
|
}
|
|
741
744
|
/**
|
|
742
|
-
*
|
|
745
|
+
* Fast retry: 5 seconds after initial connect, re-register all handlers once.
|
|
746
|
+
* Covers the case where the first batch of registrations failed silently.
|
|
747
|
+
*/
|
|
748
|
+
scheduleFastRetry(socket) {
|
|
749
|
+
this.cancelFastRetry();
|
|
750
|
+
this.fastRetryTimer = setTimeout(() => {
|
|
751
|
+
this.fastRetryTimer = null;
|
|
752
|
+
if (this.socket === socket && this.handlers.size > 0) {
|
|
753
|
+
this.logger("[RPC] Fast retry: re-registering all handlers");
|
|
754
|
+
this.registerAllHandlers(socket);
|
|
755
|
+
}
|
|
756
|
+
}, 5e3);
|
|
757
|
+
}
|
|
758
|
+
cancelFastRetry() {
|
|
759
|
+
if (this.fastRetryTimer) {
|
|
760
|
+
clearTimeout(this.fastRetryTimer);
|
|
761
|
+
this.fastRetryTimer = null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Periodic re-registration every 30s as a safety net.
|
|
743
766
|
*/
|
|
744
767
|
startReregisterInterval() {
|
|
745
768
|
this.stopReregisterInterval();
|
|
@@ -747,7 +770,7 @@ class RpcHandlerManager {
|
|
|
747
770
|
if (this.socket && this.handlers.size > 0) {
|
|
748
771
|
this.registerAllHandlers(this.socket);
|
|
749
772
|
}
|
|
750
|
-
},
|
|
773
|
+
}, 3e4);
|
|
751
774
|
}
|
|
752
775
|
stopReregisterInterval() {
|
|
753
776
|
if (this.reregisterInterval) {
|
|
@@ -1458,6 +1481,7 @@ class MachineClient {
|
|
|
1458
1481
|
keepAliveInterval = null;
|
|
1459
1482
|
tailscaleRefreshInterval = null;
|
|
1460
1483
|
lastTailscaleInfo = null;
|
|
1484
|
+
tunnelManager = null;
|
|
1461
1485
|
token;
|
|
1462
1486
|
serverUrl;
|
|
1463
1487
|
onEphemeral;
|
|
@@ -1627,6 +1651,10 @@ class MachineClient {
|
|
|
1627
1651
|
setTailscaleInfo(info) {
|
|
1628
1652
|
this.lastTailscaleInfo = info;
|
|
1629
1653
|
}
|
|
1654
|
+
/** Attach a TunnelManager for periodic tunnel state refresh. */
|
|
1655
|
+
setTunnelManager(manager) {
|
|
1656
|
+
this.tunnelManager = manager;
|
|
1657
|
+
}
|
|
1630
1658
|
shutdown() {
|
|
1631
1659
|
logger.debug("[MACHINE] Shutting down");
|
|
1632
1660
|
this.stopKeepAlive();
|
|
@@ -1658,14 +1686,22 @@ class MachineClient {
|
|
|
1658
1686
|
const info = await detectTailscale();
|
|
1659
1687
|
const serves = info.status === "connected" ? await detectTailscaleServe() : [];
|
|
1660
1688
|
const fullInfo = { ...info, serves };
|
|
1661
|
-
|
|
1689
|
+
const tsChanged = tailscaleChanged(this.lastTailscaleInfo, fullInfo);
|
|
1690
|
+
if (tsChanged) {
|
|
1662
1691
|
logger.debug(
|
|
1663
1692
|
`[MACHINE] Tailscale changed: ${this.lastTailscaleInfo?.status} \u2192 ${fullInfo.status}, serves: ${serves.length}`
|
|
1664
1693
|
);
|
|
1665
1694
|
this.lastTailscaleInfo = fullInfo;
|
|
1695
|
+
}
|
|
1696
|
+
const tunnels = this.tunnelManager ? await this.tunnelManager.detectAll() : void 0;
|
|
1697
|
+
if (tsChanged || tunnels) {
|
|
1666
1698
|
this.updateDaemonState((state) => {
|
|
1667
|
-
if (!state) return { status: "running", tailscale: fullInfo };
|
|
1668
|
-
return {
|
|
1699
|
+
if (!state) return { status: "running", tailscale: fullInfo, tunnels };
|
|
1700
|
+
return {
|
|
1701
|
+
...state,
|
|
1702
|
+
...tsChanged ? { tailscale: fullInfo } : {},
|
|
1703
|
+
...tunnels ? { tunnels } : {}
|
|
1704
|
+
};
|
|
1669
1705
|
});
|
|
1670
1706
|
}
|
|
1671
1707
|
}, TAILSCALE_REFRESH_MS);
|