@@ -310,6 +310,249 @@ fn xml_escape(value: &str) -> String {
310310 . replace ( '>' , ">" )
311311}
312312
313+ // ---------------------------------------------------------------------------
314+ // macOS privileged helper (LaunchDaemon) management
315+ // ---------------------------------------------------------------------------
316+
317+ #[ cfg( target_os = "macos" ) ]
318+ const HELPER_LABEL : & str = "io.linuxdo.accelerator.helper" ;
319+
320+ #[ cfg( target_os = "macos" ) ]
321+ fn helper_plist_path ( ) -> std:: path:: PathBuf {
322+ std:: path:: PathBuf :: from ( "/Library/LaunchDaemons" ) . join ( format ! ( "{HELPER_LABEL}.plist" ) )
323+ }
324+
325+ /// Check whether the privileged helper LaunchDaemon is installed and loaded.
326+ #[ cfg( target_os = "macos" ) ]
327+ pub fn is_privileged_helper_installed ( ) -> bool {
328+ let plist = helper_plist_path ( ) ;
329+ if !plist. exists ( ) {
330+ return false ;
331+ }
332+ // Also verify launchd knows about it.
333+ std:: process:: Command :: new ( "launchctl" )
334+ . args ( [ "list" , HELPER_LABEL ] )
335+ . output ( )
336+ . map ( |o| o. status . success ( ) )
337+ . unwrap_or ( false )
338+ }
339+
340+ /// Install the privileged helper LaunchDaemon. Requires administrator privileges
341+ /// (triggers a one-time password prompt via osascript).
342+ #[ cfg( target_os = "macos" ) ]
343+ pub fn install_privileged_helper ( exe : & std:: path:: Path , config_path : & std:: path:: Path ) -> Result < ( ) > {
344+ use std:: fs;
345+
346+ let plist_path = helper_plist_path ( ) ;
347+ let exe = absolute_display_path ( exe) ;
348+ let config = absolute_display_path ( config_path) ;
349+
350+ let plist = format ! (
351+ r#"<?xml version="1.0" encoding="UTF-8"?>
352+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
353+ <plist version="1.0">
354+ <dict>
355+ <key>Label</key>
356+ <string>{label}</string>
357+ <key>ProgramArguments</key>
358+ <array>
359+ <string>{exe}</string>
360+ <string>--config</string>
361+ <string>{config}</string>
362+ <string>privileged-helper</string>
363+ </array>
364+ <key>RunAtLoad</key>
365+ <true/>
366+ <key>KeepAlive</key>
367+ <true/>
368+ <key>ProcessType</key>
369+ <string>Standard</string>
370+ </dict>
371+ </plist>
372+ "# ,
373+ label = HELPER_LABEL ,
374+ exe = xml_escape( & exe) ,
375+ config = xml_escape( & config) ,
376+ ) ;
377+
378+ // Write the plist and load it via an elevated osascript call (one-time prompt).
379+ let script = format ! (
380+ "do shell script \" mkdir -p /Library/LaunchDaemons && cat > {plist_path} <<'PLIST_EOF'\n {plist}\n PLIST_EOF\n launchctl unload -w {plist_path} 2>/dev/null; launchctl load -w {plist_path}\" with prompt \" Linux.do Accelerator 需要安装特权辅助进程以实现免密码加速。\" with administrator privileges" ,
381+ plist_path = plist_path. display( ) ,
382+ plist = plist,
383+ ) ;
384+
385+ // We need to write the plist content through osascript because the target
386+ // directory is root-owned. Use a temp file approach for reliability.
387+ let tmp = std:: env:: temp_dir ( ) . join ( format ! ( "{HELPER_LABEL}.plist" ) ) ;
388+ fs:: write ( & tmp, & plist) . context ( "failed to write temporary plist" ) ?;
389+
390+ let install_script = format ! (
391+ "do shell script \" cp '{tmp}' '{dest}' && launchctl unload -w '{dest}' 2>/dev/null; launchctl load -w '{dest}'\" with prompt \" Linux.do Accelerator 需要安装特权辅助进程以实现免密码加速。\" with administrator privileges" ,
392+ tmp = tmp. display( ) ,
393+ dest = plist_path. display( ) ,
394+ ) ;
395+
396+ crate :: platform:: run_command ( "osascript" , & [ "-e" , & install_script] )
397+ . context ( "failed to install privileged helper (osascript)" ) ?;
398+
399+ // Clean up temp file.
400+ let _ = fs:: remove_file ( & tmp) ;
401+
402+ // Give launchd a moment to start the helper.
403+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 500 ) ) ;
404+
405+ Ok ( ( ) )
406+ }
407+
408+ /// Uninstall the privileged helper LaunchDaemon.
409+ #[ cfg( target_os = "macos" ) ]
410+ pub fn uninstall_privileged_helper ( ) -> Result < ( ) > {
411+ let plist_path = helper_plist_path ( ) ;
412+ if !plist_path. exists ( ) {
413+ return Ok ( ( ) ) ;
414+ }
415+
416+ let script = format ! (
417+ "do shell script \" launchctl unload -w '{plist}' 2>/dev/null; rm -f '{plist}'\" with prompt \" Linux.do Accelerator 需要卸载特权辅助进程。\" with administrator privileges" ,
418+ plist = plist_path. display( ) ,
419+ ) ;
420+
421+ crate :: platform:: run_command ( "osascript" , & [ "-e" , & script] )
422+ . context ( "failed to uninstall privileged helper" ) ?;
423+ Ok ( ( ) )
424+ }
425+
426+ // ---------------------------------------------------------------------------
427+ // Linux privileged helper (systemd user service) management
428+ // ---------------------------------------------------------------------------
429+
430+ #[ cfg( target_os = "linux" ) ]
431+ const HELPER_SERVICE_NAME : & str = "linuxdo-accelerator-helper" ;
432+
433+ #[ cfg( target_os = "linux" ) ]
434+ fn helper_service_path ( ) -> Result < PathBuf > {
435+ // Use a system-level service so it runs as root without per-user setup.
436+ Ok ( PathBuf :: from ( "/etc/systemd/system" )
437+ . join ( format ! ( "{HELPER_SERVICE_NAME}.service" ) ) )
438+ }
439+
440+ /// Check whether the privileged helper systemd service is installed and active.
441+ #[ cfg( target_os = "linux" ) ]
442+ pub fn is_privileged_helper_installed ( ) -> bool {
443+ let Ok ( path) = helper_service_path ( ) else {
444+ return false ;
445+ } ;
446+ if !path. exists ( ) {
447+ return false ;
448+ }
449+ std:: process:: Command :: new ( "systemctl" )
450+ . args ( [ "is-active" , "--quiet" , HELPER_SERVICE_NAME ] )
451+ . status ( )
452+ . map ( |s| s. success ( ) )
453+ . unwrap_or ( false )
454+ }
455+
456+ /// Install the privileged helper as a systemd service. Requires root (pkexec).
457+ #[ cfg( target_os = "linux" ) ]
458+ pub fn install_privileged_helper ( exe : & std:: path:: Path , config_path : & std:: path:: Path ) -> Result < ( ) > {
459+ use std:: fs;
460+
461+ let service_path = helper_service_path ( ) ?;
462+ let exe = exe. canonicalize ( ) . unwrap_or_else ( |_| exe. to_path_buf ( ) ) ;
463+ let config = config_path. canonicalize ( ) . unwrap_or_else ( |_| config_path. to_path_buf ( ) ) ;
464+
465+ let unit = format ! (
466+ r#"[Unit]
467+ Description=Linux.do Accelerator Privileged Helper
468+ After=network.target
469+
470+ [Service]
471+ Type=simple
472+ ExecStart={exe} --config {config} privileged-helper
473+ Restart=on-failure
474+ RestartSec=3
475+
476+ [Install]
477+ WantedBy=multi-user.target
478+ "# ,
479+ exe = exe. display( ) ,
480+ config = config. display( ) ,
481+ ) ;
482+
483+ // Write the unit file via pkexec (one-time root prompt).
484+ let tmp = std:: env:: temp_dir ( ) . join ( format ! ( "{HELPER_SERVICE_NAME}.service" ) ) ;
485+ fs:: write ( & tmp, & unit) . context ( "failed to write temporary service file" ) ?;
486+
487+ let dest = service_path. display ( ) ;
488+ let status = std:: process:: Command :: new ( "pkexec" )
489+ . args ( [
490+ "bash" ,
491+ "-c" ,
492+ & format ! (
493+ "cp '{tmp}' '{dest}' && systemctl daemon-reload && systemctl enable --now '{name}'" ,
494+ tmp = tmp. display( ) ,
495+ dest = dest,
496+ name = HELPER_SERVICE_NAME ,
497+ ) ,
498+ ] )
499+ . status ( )
500+ . context ( "failed to execute pkexec" ) ?;
501+
502+ let _ = fs:: remove_file ( & tmp) ;
503+
504+ if !status. success ( ) {
505+ bail ! ( "pkexec rejected the request or systemctl failed" ) ;
506+ }
507+
508+ // Give systemd a moment to start the helper.
509+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 500 ) ) ;
510+ Ok ( ( ) )
511+ }
512+
513+ /// Uninstall the privileged helper systemd service.
514+ #[ cfg( target_os = "linux" ) ]
515+ pub fn uninstall_privileged_helper ( ) -> Result < ( ) > {
516+ let Ok ( path) = helper_service_path ( ) else {
517+ return Ok ( ( ) ) ;
518+ } ;
519+ if !path. exists ( ) {
520+ return Ok ( ( ) ) ;
521+ }
522+
523+ let status = std:: process:: Command :: new ( "pkexec" )
524+ . args ( [
525+ "bash" ,
526+ "-c" ,
527+ & format ! (
528+ "systemctl disable --now '{name}' 2>/dev/null; rm -f '{path}' && systemctl daemon-reload" ,
529+ name = HELPER_SERVICE_NAME ,
530+ path = path. display( ) ,
531+ ) ,
532+ ] )
533+ . status ( )
534+ . context ( "failed to execute pkexec" ) ?;
535+
536+ if !status. success ( ) {
537+ bail ! ( "pkexec rejected the request or systemctl failed" ) ;
538+ }
539+ Ok ( ( ) )
540+ }
541+
542+ // Non-Unix stubs (Windows, Android, etc.)
543+ #[ cfg( not( any( target_os = "macos" , target_os = "linux" ) ) ) ]
544+ pub fn is_privileged_helper_installed ( ) -> bool {
545+ false
546+ }
547+ #[ cfg( not( any( target_os = "macos" , target_os = "linux" ) ) ) ]
548+ pub fn install_privileged_helper ( _exe : & std:: path:: Path , _config_path : & std:: path:: Path ) -> Result < ( ) > {
549+ bail ! ( "privileged helper is not supported on this platform" )
550+ }
551+ #[ cfg( not( any( target_os = "macos" , target_os = "linux" ) ) ) ]
552+ pub fn uninstall_privileged_helper ( ) -> Result < ( ) > {
553+ bail ! ( "privileged helper is not supported on this platform" )
554+ }
555+
313556#[ cfg( target_os = "linux" ) ]
314557fn platform_enable ( exe : & Path , config_path : & Path ) -> Result < ( ) > {
315558 use std:: fs;
0 commit comments