@@ -26,6 +26,19 @@ use self::core as tailscale_core;
2626#[ cfg( any( target_os = "android" , target_os = "ios" ) ) ]
2727const UNSUPPORTED_MESSAGE : & str = "Tailscale integration is only available on desktop." ;
2828
29+ #[ cfg( target_os = "macos" ) ]
30+ fn tailscale_command ( binary : & OsStr ) -> tokio:: process:: Command {
31+ let mut command = tokio_command ( "/bin/launchctl" ) ;
32+ let uid = unsafe { libc:: geteuid ( ) } ;
33+ command. arg ( "asuser" ) . arg ( uid. to_string ( ) ) . arg ( binary) ;
34+ command
35+ }
36+
37+ #[ cfg( not( target_os = "macos" ) ) ]
38+ fn tailscale_command ( binary : & OsStr ) -> tokio:: process:: Command {
39+ tokio_command ( binary)
40+ }
41+
2942fn trim_to_non_empty ( value : Option < & str > ) -> Option < String > {
3043 value
3144 . map ( str:: trim)
@@ -79,9 +92,28 @@ fn missing_tailscale_message() -> String {
7992async fn resolve_tailscale_binary ( ) -> Result < Option < ( OsString , Output ) > , String > {
8093 let mut failures: Vec < String > = Vec :: new ( ) ;
8194 for binary in tailscale_binary_candidates ( ) {
82- let output = tokio_command ( & binary) . arg ( "version" ) . output ( ) . await ;
95+ let output = tailscale_command ( binary. as_os_str ( ) )
96+ . arg ( "version" )
97+ . output ( )
98+ . await ;
8399 match output {
84- Ok ( version_output) => return Ok ( Some ( ( binary, version_output) ) ) ,
100+ Ok ( version_output) => {
101+ if version_output. status . success ( ) {
102+ return Ok ( Some ( ( binary, version_output) ) ) ;
103+ }
104+ let stdout = trim_to_non_empty ( std:: str:: from_utf8 ( & version_output. stdout ) . ok ( ) ) ;
105+ let stderr = trim_to_non_empty ( std:: str:: from_utf8 ( & version_output. stderr ) . ok ( ) ) ;
106+ let detail = match ( stdout, stderr) {
107+ ( Some ( out) , Some ( err) ) => format ! ( "stdout: {out}; stderr: {err}" ) ,
108+ ( Some ( out) , None ) => format ! ( "stdout: {out}" ) ,
109+ ( None , Some ( err) ) => format ! ( "stderr: {err}" ) ,
110+ ( None , None ) => "no output" . to_string ( ) ,
111+ } ;
112+ failures. push ( format ! (
113+ "{}: tailscale version failed ({detail})" ,
114+ OsStr :: new( & binary) . to_string_lossy( )
115+ ) ) ;
116+ }
85117 Err ( err) if err. kind ( ) == ErrorKind :: NotFound => continue ,
86118 Err ( err) => failures. push ( format ! ( "{}: {err}" , OsStr :: new( & binary) . to_string_lossy( ) ) ) ,
87119 }
@@ -311,7 +343,7 @@ pub(crate) async fn tailscale_status() -> Result<TailscaleStatus, String> {
311343 let version = trim_to_non_empty ( std:: str:: from_utf8 ( & version_output. stdout ) . ok ( ) )
312344 . and_then ( |raw| raw. lines ( ) . next ( ) . map ( str:: trim) . map ( str:: to_string) ) ;
313345
314- let status_output = tokio_command ( & tailscale_binary)
346+ let status_output = tailscale_command ( tailscale_binary. as_os_str ( ) )
315347 . arg ( "status" )
316348 . arg ( "--json" )
317349 . output ( )
@@ -337,7 +369,41 @@ pub(crate) async fn tailscale_status() -> Result<TailscaleStatus, String> {
337369
338370 let payload = std:: str:: from_utf8 ( & status_output. stdout )
339371 . map_err ( |err| format ! ( "Invalid UTF-8 from tailscale status: {err}" ) ) ?;
340- tailscale_core:: status_from_json ( version, payload)
372+ let stderr_text = trim_to_non_empty ( std:: str:: from_utf8 ( & status_output. stderr ) . ok ( ) ) ;
373+ if payload. trim ( ) . is_empty ( ) {
374+ let suffix = stderr_text
375+ . as_deref ( )
376+ . map ( |value| format ! ( " stderr: {value}" ) )
377+ . unwrap_or_default ( ) ;
378+ return Err ( format ! (
379+ "tailscale status --json returned empty output.{suffix}"
380+ ) ) ;
381+ }
382+ match tailscale_core:: status_from_json ( version, payload) {
383+ Ok ( status) => Ok ( status) ,
384+ Err ( err) => {
385+ let trimmed_payload = payload. trim ( ) ;
386+ let payload_preview = if trimmed_payload. is_empty ( ) {
387+ None
388+ } else if trimmed_payload. len ( ) > 200 {
389+ Some ( format ! ( "{}…" , & trimmed_payload[ ..200 ] ) )
390+ } else {
391+ Some ( trimmed_payload. to_string ( ) )
392+ } ;
393+ let mut details = Vec :: new ( ) ;
394+ if let Some ( stderr) = stderr_text {
395+ details. push ( format ! ( "stderr: {stderr}" ) ) ;
396+ }
397+ if let Some ( preview) = payload_preview {
398+ details. push ( format ! ( "stdout: {preview}" ) ) ;
399+ }
400+ if details. is_empty ( ) {
401+ Err ( err)
402+ } else {
403+ Err ( format ! ( "{err} ({})" , details. join( "; " ) ) )
404+ }
405+ }
406+ }
341407}
342408
343409#[ cfg( test) ]
0 commit comments