@@ -12,6 +12,9 @@ use super::SOCKET_ENV;
1212
1313const LAUNCHD_LABEL : & str = "com.tracedecay.daemon" ;
1414const LAUNCHD_PLIST_NAME : & str = "com.tracedecay.daemon.plist" ;
15+ // Cached project owners retain SQLite families and coordination locks. The
16+ // platform default of 256 descriptors is too small for multi-worktree use.
17+ const DAEMON_OPEN_FILE_LIMIT : u32 = 8_192 ;
1518
1619#[ derive( Debug , Clone , PartialEq , Eq ) ]
1720pub struct DaemonServiceSpec {
@@ -54,12 +57,14 @@ impl DaemonServiceSpec {
5457 ExecStart={} daemon run --socket {}\n \
5558 Restart=on-failure\n \
5659 RestartSec=2\n \
60+ LimitNOFILE={}\n \
5761 \n \
5862 [Install]\n \
5963 WantedBy=default.target\n ",
6064 systemd_escape_env_value( & service_path) ,
6165 self . tracedecay_bin. display( ) ,
62- self . socket_path. display( )
66+ self . socket_path. display( ) ,
67+ DAEMON_OPEN_FILE_LIMIT ,
6368 )
6469 }
6570
@@ -136,6 +141,12 @@ impl DaemonServiceSpec {
136141 \n \
137142 <key>ThrottleInterval</key>\n \
138143 <integer>2</integer>\n \
144+ \n \
145+ <key>SoftResourceLimits</key>\n \
146+ <dict>\n \
147+ <key>NumberOfFiles</key>\n \
148+ <integer>{open_file_limit}</integer>\n \
149+ </dict>\n \
139150 \n \
140151 <key>StandardOutPath</key>\n \
141152 <string>{stdout}</string>\n \
@@ -147,6 +158,7 @@ impl DaemonServiceSpec {
147158 label = plist_xml_escape( LAUNCHD_LABEL ) ,
148159 bin = plist_xml_escape( & self . tracedecay_bin. display( ) . to_string( ) ) ,
149160 socket = plist_xml_escape( & self . socket_path. display( ) . to_string( ) ) ,
161+ open_file_limit = DAEMON_OPEN_FILE_LIMIT ,
150162 stdout = plist_xml_escape( & data_dir. join( "daemon.out.log" ) . display( ) . to_string( ) ) ,
151163 stderr = plist_xml_escape( & data_dir. join( "daemon.err.log" ) . display( ) . to_string( ) ) ,
152164 ) )
@@ -373,8 +385,41 @@ fn refresh_installed_service_with_state(
373385 refresh_service_with_runner ( & runner, & refreshed_spec, previous_state) . map ( Some )
374386}
375387
388+ /// Stops a managed daemon before `daemon restart` acquires exclusive lifecycle
389+ /// ownership. The daemon itself holds a shared lease while it is running.
390+ #[ doc( hidden) ]
391+ pub fn quiesce_installed_service_for_restart ( ) -> Result < DaemonServiceState > {
392+ quiesce_installed_service ( )
393+ }
394+
376395#[ doc( hidden) ]
377396pub fn quiesce_installed_service_under_lease ( ) -> Result < DaemonServiceState > {
397+ quiesce_installed_service ( )
398+ }
399+
400+ /// Restores an installed service that was running before restart quiesced it.
401+ /// This deliberately reuses the existing unit instead of rewriting it because
402+ /// the caller invokes it only after exclusive lifecycle acquisition failed.
403+ #[ doc( hidden) ]
404+ pub fn restore_quiesced_installed_service ( previous_state : DaemonServiceState ) -> Result < ( ) > {
405+ if !cfg ! ( any( target_os = "linux" , target_os = "macos" ) ) || !previous_state. is_running ( ) {
406+ return Ok ( ( ) ) ;
407+ }
408+ let service_path = service_unit_path ( ) ?;
409+ if !service_path. exists ( ) {
410+ return Err ( TraceDecayError :: Config {
411+ message : format ! (
412+ "cannot restore missing TraceDecay daemon service '{}'" ,
413+ service_path. display( )
414+ ) ,
415+ } ) ;
416+ }
417+ let unit = read_service_unit ( & service_path) ?;
418+ let socket_path = socket_path_from_unit_text ( & unit) . unwrap_or ( default_socket_path ( ) ?) ;
419+ ServiceRunner :: current ( ) ?. restore_after_quiesce ( & service_path, & socket_path, previous_state)
420+ }
421+
422+ fn quiesce_installed_service ( ) -> Result < DaemonServiceState > {
378423 if !cfg ! ( any( target_os = "linux" , target_os = "macos" ) ) {
379424 return Ok ( DaemonServiceState :: Missing ) ;
380425 }
@@ -758,6 +803,27 @@ impl ServiceRunner {
758803 }
759804 }
760805
806+ fn restore_after_quiesce (
807+ & self ,
808+ service_path : & Path ,
809+ socket_path : & Path ,
810+ previous_state : DaemonServiceState ,
811+ ) -> Result < ( ) > {
812+ if !previous_state. is_running ( ) {
813+ return Ok ( ( ) ) ;
814+ }
815+ match self {
816+ Self :: Systemd => run_systemctl ( & [ "start" , super :: SERVICE_NAME ] ) ,
817+ Self :: Launchd => {
818+ launchd_refresh ( service_path, socket_path) ?;
819+ if !previous_state. is_enabled ( ) {
820+ run_launchctl ( & [ "disable" , & launchd_service_target ( ) ?] ) ?;
821+ }
822+ Ok ( ( ) )
823+ }
824+ }
825+ }
826+
761827 fn after_uninstall ( & self , stop : bool ) {
762828 match self {
763829 Self :: Systemd => {
@@ -1224,6 +1290,7 @@ mod tests {
12241290 ) ) ;
12251291 assert ! ( unit. contains( "Environment=\" PATH=" ) ) ;
12261292 assert ! ( unit. contains( "Restart=on-failure" ) ) ;
1293+ assert ! ( unit. contains( "LimitNOFILE=8192" ) ) ;
12271294 }
12281295
12291296 // The launchd render tests use Unix-style absolute binary paths, which
@@ -1265,6 +1332,9 @@ mod tests {
12651332 assert ! ( plist. contains( "<key>TRACEDECAY_DATA_DIR</key>" ) ) ;
12661333 assert ! ( plist. contains( "<key>RunAtLoad</key>" ) ) ;
12671334 assert ! ( plist. contains( "<key>KeepAlive</key>" ) ) ;
1335+ assert ! ( plist. contains( "<key>SoftResourceLimits</key>" ) ) ;
1336+ assert ! ( plist. contains( "<key>NumberOfFiles</key>" ) ) ;
1337+ assert ! ( plist. contains( "<integer>8192</integer>" ) ) ;
12681338 }
12691339
12701340 #[ cfg( unix) ]
@@ -1619,6 +1689,55 @@ mod tests {
16191689 ) ;
16201690 }
16211691
1692+ #[ cfg( target_os = "linux" ) ]
1693+ #[ test]
1694+ fn restore_quiesced_service_starts_existing_unit_without_rewriting_it ( ) {
1695+ let _env_lock = lock_user_data_dir_test_env ( ) ;
1696+ let dir = TempDir :: new ( ) . expect ( "temp dir" ) ;
1697+ let config_home = dir. path ( ) . join ( "config" ) ;
1698+ let fake_bin = dir. path ( ) . join ( "bin" ) ;
1699+ let home = dir. path ( ) . join ( "home" ) ;
1700+ std:: fs:: create_dir_all ( & fake_bin) . expect ( "fake bin dir" ) ;
1701+ std:: fs:: create_dir_all ( & home) . expect ( "home dir" ) ;
1702+
1703+ let systemctl = fake_bin. join ( "systemctl" ) ;
1704+ let log = dir. path ( ) . join ( "systemctl.log" ) ;
1705+ std:: fs:: write (
1706+ & systemctl,
1707+ "#!/bin/sh\n printf '%s\\ n' \" $*\" >> \" $TRACEDECAY_SYSTEMCTL_LOG\" \n exit 0\n " ,
1708+ )
1709+ . expect ( "fake systemctl" ) ;
1710+ std:: fs:: set_permissions ( & systemctl, std:: fs:: Permissions :: from_mode ( 0o755 ) )
1711+ . expect ( "systemctl permissions" ) ;
1712+
1713+ let _config_guard = EnvVarGuard :: set ( "XDG_CONFIG_HOME" , & config_home) ;
1714+ let _home_guard = EnvVarGuard :: set ( "HOME" , & home) ;
1715+ let _data_guard =
1716+ EnvVarGuard :: set ( crate :: config:: USER_DATA_DIR_ENV , dir. path ( ) . join ( "profile" ) ) ;
1717+ let _path_guard = EnvVarGuard :: set ( "PATH" , & fake_bin) ;
1718+ let _log_guard = EnvVarGuard :: set ( "TRACEDECAY_SYSTEMCTL_LOG" , & log) ;
1719+ let service_path = config_home
1720+ . join ( "systemd/user" )
1721+ . join ( crate :: daemon:: SERVICE_NAME ) ;
1722+ std:: fs:: create_dir_all ( service_path. parent ( ) . expect ( "service parent" ) )
1723+ . expect ( "service dir" ) ;
1724+ let original_unit =
1725+ "[Service]\n ExecStart=/old/tracedecay daemon run --socket /custom/tracedecay.sock\n " ;
1726+ std:: fs:: write ( & service_path, original_unit) . expect ( "existing service unit" ) ;
1727+
1728+ super :: restore_quiesced_installed_service ( DaemonServiceState :: RunningEnabled )
1729+ . expect ( "restore service" ) ;
1730+
1731+ assert_eq ! (
1732+ std:: fs:: read_to_string( service_path) . expect( "service unit" ) ,
1733+ original_unit
1734+ ) ;
1735+ assert_eq ! (
1736+ std:: fs:: read_to_string( log) . expect( "systemctl log" ) ,
1737+ "--user start tracedecay.service\n "
1738+ ) ;
1739+ }
1740+
16221741 #[ cfg( target_os = "linux" ) ]
16231742 #[ test]
16241743 fn refresh_installed_service_preserves_stopped_state ( ) {
0 commit comments