11// Integrates with the local Tailscale CLI for tailnet setup and sharing.
22import { existsSync } from "node:fs" ;
3- import { setTimeout as delay } from "node:timers/promises" ;
43import {
54 asDateTimestampMs ,
65 resolveExpiresAtMsFromDurationMs ,
@@ -13,6 +12,7 @@ import {
1312import { logVerbose } from "../globals.js" ;
1413import { runExec } from "../process/exec.js" ;
1514import { toErrorObject } from "./errors.js" ;
15+ import { readTailscaleStatusJson } from "./tailscale-status.js" ;
1616
1717function parsePossiblyNoisyJsonObject ( stdout : string ) : Record < string , unknown > {
1818 const trimmed = stdout . trim ( ) ;
@@ -24,54 +24,6 @@ function parsePossiblyNoisyJsonObject(stdout: string): Record<string, unknown> {
2424 return JSON . parse ( trimmed ) as Record < string , unknown > ;
2525}
2626
27- const TAILSCALE_STATUS_ATTEMPTS = 3 ;
28- const TAILSCALE_STATUS_RETRY_DELAY_MS = 500 ;
29-
30- function isTransientTailscaleStatusError ( error : unknown ) : boolean {
31- const record = readRecord ( error ) ;
32- const timedOut = record ?. killed === true && record . signal === "SIGTERM" ;
33- const detail = [
34- error instanceof Error ? error . message : undefined ,
35- typeof record ?. stderr === "string" ? record . stderr : undefined ,
36- typeof record ?. stdout === "string" ? record . stdout : undefined ,
37- ]
38- . filter ( ( value ) : value is string => Boolean ( value ) )
39- . join ( "\n" )
40- . toLowerCase ( ) ;
41-
42- return (
43- timedOut ||
44- detail . includes ( "failed to connect to local tailscale" ) ||
45- detail . includes ( "connection refused" ) ||
46- detail . includes ( "503 service unavailable: no backend" )
47- ) ;
48- }
49-
50- async function readTailscaleStatusJson (
51- candidate : string ,
52- exec : typeof runExec ,
53- ) : Promise < Record < string , unknown > > {
54- for ( let attempt = 1 ; attempt <= TAILSCALE_STATUS_ATTEMPTS ; attempt += 1 ) {
55- let stdout : string ;
56- try {
57- ( { stdout } = await exec ( candidate , [ "status" , "--json" ] , {
58- timeoutMs : 5000 ,
59- maxBuffer : 400_000 ,
60- } ) ) ;
61- } catch ( error ) {
62- if ( ! isTransientTailscaleStatusError ( error ) || attempt === TAILSCALE_STATUS_ATTEMPTS ) {
63- throw toErrorObject ( error , "Non-Error thrown" ) ;
64- }
65- // Serve startup can briefly race daemon status. Retry the same detected binary so
66- // binary fallback does not hide the transient failure or change installations.
67- await delay ( TAILSCALE_STATUS_RETRY_DELAY_MS ) ;
68- continue ;
69- }
70- return stdout ? parsePossiblyNoisyJsonObject ( stdout ) : { } ;
71- }
72- throw new Error ( "Tailscale status retry loop exhausted" ) ;
73- }
74-
7527/**
7628 * Locate Tailscale binary using multiple strategies:
7729 * 1. PATH lookup (via which command)
@@ -179,7 +131,7 @@ export async function getTailnetHostname(exec: typeof runExec = runExec, detecte
179131 continue ;
180132 }
181133 try {
182- const parsed = await readTailscaleStatusJson ( candidate , exec ) ;
134+ const parsed = await readTailscaleStatusJson ( candidate , exec , parsePossiblyNoisyJsonObject ) ;
183135 const self =
184136 typeof parsed . Self === "object" && parsed . Self !== null
185137 ? ( parsed . Self as Record < string , unknown > )
0 commit comments