1111//! `~/.config/systemd/user/burnwall.service`. `Restart=on-failure` with
1212//! `StartLimitBurst=5` + `StartLimitIntervalSec=60` is the same crash-loop
1313//! circuit breaker shape.
14- //! - **Windows** — a per-user Scheduled Task triggered at logon, registered
15- //! via `schtasks.exe`. Task Scheduler restarts on failure (5 attempts at
16- //! 1-min intervals) — same shape, different incantation.
14+ //! - **Windows** — by default, a per-user `HKCU\…\CurrentVersion\Run` registry
15+ //! entry that launches `burnwall start --daemon` at logon. This needs **no
16+ //! admin / UAC** (the earlier Scheduled-Task default failed with "Access is
17+ //! denied" because creating a task at the library root requires elevation).
18+ //! `--task` opts into the Scheduled-Task variant instead — it adds
19+ //! crash-restart (5 attempts at 1-min intervals) but must be run from an
20+ //! elevated terminal.
1721//!
18- //! ## No admin required
22+ //! ## No admin required (by default)
1923//!
20- //! All three install user-scoped services that need no admin / sudo / UAC.
21- //! Per-user is the right scope because the proxy serves one user's traffic
22- //! through env vars in their shell.
24+ //! Every default path installs a user-scoped service that needs no admin /
25+ //! sudo / UAC. Per-user is the right scope because the proxy serves one user's
26+ //! traffic through env vars in their shell. (Windows `--task` is the one opt-in
27+ //! that needs elevation, in exchange for crash-restart.)
2328
2429use std:: path:: PathBuf ;
2530
@@ -36,14 +41,19 @@ pub struct InstallServiceArgs {
3641 /// Skip the start step (just register the service, don't launch it).
3742 #[ arg( long) ]
3843 pub no_start : bool ,
44+ /// Windows only: register a Scheduled Task (adds crash-restart) instead of
45+ /// the default per-user Run-key entry. Must be run from an elevated
46+ /// terminal. Ignored on macOS/Linux.
47+ #[ arg( long) ]
48+ pub task : bool ,
3949}
4050
4151#[ derive( Args , Debug ) ]
4252pub struct UninstallServiceArgs { }
4353
4454pub fn install_cmd ( args : InstallServiceArgs ) -> Result < ( ) > {
4555 let exe = std:: env:: current_exe ( ) . context ( "locating burnwall executable" ) ?;
46- install ( & exe, !args. no_start )
56+ install ( & exe, !args. no_start , args . task )
4757}
4858
4959pub fn uninstall_cmd ( _args : UninstallServiceArgs ) -> Result < ( ) > {
@@ -93,7 +103,7 @@ fn plist_contents(exe: &std::path::Path) -> String {
93103}
94104
95105#[ cfg( target_os = "macos" ) ]
96- fn install ( exe : & std:: path:: Path , start : bool ) -> Result < ( ) > {
106+ fn install ( exe : & std:: path:: Path , start : bool , _task : bool ) -> Result < ( ) > {
97107 let path = plist_path ( ) ?;
98108 if let Some ( parent) = path. parent ( ) {
99109 std:: fs:: create_dir_all ( parent)
@@ -170,7 +180,7 @@ WantedBy=default.target
170180}
171181
172182#[ cfg( target_os = "linux" ) ]
173- fn install ( exe : & std:: path:: Path , start : bool ) -> Result < ( ) > {
183+ fn install ( exe : & std:: path:: Path , start : bool , _task : bool ) -> Result < ( ) > {
174184 let path = unit_path ( ) ?;
175185 if let Some ( parent) = path. parent ( ) {
176186 std:: fs:: create_dir_all ( parent)
@@ -292,8 +302,56 @@ fn task_xml(exe: &std::path::Path) -> String {
292302 )
293303}
294304
305+ /// HKCU autostart key — writable by a standard user, no admin needed.
306+ #[ cfg( target_os = "windows" ) ]
307+ const RUN_KEY : & str = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run" ;
308+
309+ #[ cfg( target_os = "windows" ) ]
310+ fn install ( exe : & std:: path:: Path , start : bool , use_task : bool ) -> Result < ( ) > {
311+ if use_task {
312+ install_scheduled_task ( exe, start)
313+ } else {
314+ install_run_key ( exe, start)
315+ }
316+ }
317+
318+ /// Default Windows autostart: a per-user `HKCU\…\Run` value that launches
319+ /// `burnwall start --daemon` at logon. No admin required. Written via `reg.exe`
320+ /// so we don't pull in a registry crate.
321+ #[ cfg( target_os = "windows" ) ]
322+ fn install_run_key ( exe : & std:: path:: Path , start : bool ) -> Result < ( ) > {
323+ // The exe path is quoted so a profile path with spaces still parses at logon.
324+ let command = format ! ( "\" {}\" start --daemon" , exe. display( ) ) ;
325+ let status = std:: process:: Command :: new ( "reg" )
326+ . args ( [
327+ "add" , RUN_KEY , "/v" , TASK_NAME , "/t" , "REG_SZ" , "/d" , & command, "/f" ,
328+ ] )
329+ . stdout ( std:: process:: Stdio :: null ( ) )
330+ . stderr ( std:: process:: Stdio :: null ( ) )
331+ . status ( )
332+ . context ( "running reg add" ) ?;
333+ if !status. success ( ) {
334+ anyhow:: bail!(
335+ "reg add failed (status {status}). You can still run `burnwall start --daemon` \
336+ manually, or try `burnwall install-service --task` from an elevated terminal."
337+ ) ;
338+ }
339+ println ! ( "🛡 Registered login auto-start (HKCU Run): {TASK_NAME}" ) ;
340+ println ! ( " Launches `burnwall start --daemon` at logon — no admin required." ) ;
341+ if start {
342+ start_daemon_now ( exe) ;
343+ } else {
344+ println ! ( " (not started — will start at next logon)" ) ;
345+ }
346+ println ! ( " Tip: `--task` installs a Scheduled Task with crash-restart (needs an elevated terminal)." ) ;
347+ Ok ( ( ) )
348+ }
349+
350+ /// Opt-in Windows autostart: a per-user Scheduled Task at logon. Adds
351+ /// crash-restart, but creating the task at the library root requires
352+ /// elevation — so this must be run from an Administrator terminal.
295353#[ cfg( target_os = "windows" ) ]
296- fn install ( exe : & std:: path:: Path , start : bool ) -> Result < ( ) > {
354+ fn install_scheduled_task ( exe : & std:: path:: Path , start : bool ) -> Result < ( ) > {
297355 let xml_path = task_xml_path ( ) ?;
298356 if let Some ( parent) = xml_path. parent ( ) {
299357 std:: fs:: create_dir_all ( parent)
@@ -320,15 +378,23 @@ fn install(exe: &std::path::Path, start: bool) -> Result<()> {
320378 "/XML" ,
321379 xml_path. to_str ( ) . unwrap_or ( "" ) ,
322380 ] )
381+ . stdout ( std:: process:: Stdio :: null ( ) )
382+ . stderr ( std:: process:: Stdio :: null ( ) )
323383 . status ( )
324384 . context ( "running schtasks /Create" ) ?;
325385 if !status. success ( ) {
326- anyhow:: bail!( "schtasks /Create failed (status {})" , status) ;
386+ anyhow:: bail!(
387+ "schtasks /Create failed (status {status}) — this usually means it wasn't run \
388+ elevated. Run from an Administrator terminal, or drop `--task` to use the \
389+ no-admin Run-key install instead."
390+ ) ;
327391 }
328392 println ! ( "🛡 Installed Scheduled Task: \\ {TASK_NAME}" ) ;
329393 if start {
330394 let s = std:: process:: Command :: new ( "schtasks.exe" )
331395 . args ( [ "/Run" , "/TN" , TASK_NAME ] )
396+ . stdout ( std:: process:: Stdio :: null ( ) )
397+ . stderr ( std:: process:: Stdio :: null ( ) )
332398 . status ( )
333399 . context ( "running schtasks /Run" ) ?;
334400 if !s. success ( ) {
@@ -344,17 +410,48 @@ fn install(exe: &std::path::Path, start: bool) -> Result<()> {
344410}
345411
346412#[ cfg( target_os = "windows" ) ]
347- fn uninstall ( ) -> Result < ( ) > {
348- let status = std:: process:: Command :: new ( "schtasks. exe" )
349- . args ( [ "/Delete " , "/F" , "/TN" , TASK_NAME ] )
413+ fn start_daemon_now ( exe : & std :: path :: Path ) {
414+ match std:: process:: Command :: new ( exe)
415+ . args ( [ "start " , "--daemon" ] )
350416 . status ( )
351- . context ( "running schtasks /Delete" ) ?;
352- if status. success ( ) {
417+ {
418+ Ok ( s) if s. success ( ) => println ! ( " Started." ) ,
419+ _ => println ! ( " (could not start now — will start at next logon)" ) ,
420+ }
421+ }
422+
423+ #[ cfg( target_os = "windows" ) ]
424+ fn uninstall ( ) -> Result < ( ) > {
425+ let mut removed = false ;
426+ // Default install: the HKCU Run-key value. Probes are best-effort — silence
427+ // child stdout/stderr so a missing entry doesn't print a scary "ERROR".
428+ if matches ! (
429+ std:: process:: Command :: new( "reg" )
430+ . args( [ "delete" , RUN_KEY , "/v" , TASK_NAME , "/f" ] )
431+ . stdout( std:: process:: Stdio :: null( ) )
432+ . stderr( std:: process:: Stdio :: null( ) )
433+ . status( ) ,
434+ Ok ( s) if s. success( )
435+ ) {
436+ println ! ( "🛡 Removed login auto-start (HKCU Run): {TASK_NAME}" ) ;
437+ removed = true ;
438+ }
439+ // Opt-in install: the Scheduled Task.
440+ if matches ! (
441+ std:: process:: Command :: new( "schtasks.exe" )
442+ . args( [ "/Delete" , "/F" , "/TN" , TASK_NAME ] )
443+ . stdout( std:: process:: Stdio :: null( ) )
444+ . stderr( std:: process:: Stdio :: null( ) )
445+ . status( ) ,
446+ Ok ( s) if s. success( )
447+ ) {
353448 println ! ( "🛡 Removed Scheduled Task: \\ {TASK_NAME}" ) ;
354- } else {
355- println ! ( "🛡 No Scheduled Task to remove (or removal failed)." ) ;
449+ removed = true ;
450+ }
451+ if !removed {
452+ println ! ( "🛡 No Burnwall login service found to remove." ) ;
356453 }
357- // Best-effort cleanup of the staged XML.
454+ // Best-effort cleanup of any staged task XML.
358455 if let Ok ( xml_path) = task_xml_path ( ) {
359456 let _ = std:: fs:: remove_file ( & xml_path) ;
360457 }
@@ -364,7 +461,7 @@ fn uninstall() -> Result<()> {
364461// ─────────────────────────── unsupported ───────────────────────────
365462
366463#[ cfg( not( any( target_os = "macos" , target_os = "linux" , target_os = "windows" ) ) ) ]
367- fn install ( _exe : & std:: path:: Path , _start : bool ) -> Result < ( ) > {
464+ fn install ( _exe : & std:: path:: Path , _start : bool , _task : bool ) -> Result < ( ) > {
368465 anyhow:: bail!( "install-service is not supported on this OS" ) ;
369466}
370467
0 commit comments