@@ -570,6 +570,53 @@ impl AppState {
570570 }
571571 }
572572
573+ /// Shut the VSS helper broker down BEFORE an in-app update installer runs
574+ /// (issue #125), returning whether a manager was disabled (so the caller can
575+ /// RE-ARM it if the install then fails - see
576+ /// [`Self::rearm_vss_helper_after_failed_update`]).
577+ ///
578+ /// The Windows NSIS updater overwrites the bundled `driven-vss-helper.exe`
579+ /// sidecar, but its stock process-kill only targets the MAIN binary
580+ /// (`driven-app.exe`) - never the sidecar. A running elevated broker holds an
581+ /// open handle to its own exe, so the install fails with "Error opening file
582+ /// for writing: ...driven-vss-helper.exe". `download_and_install` runs the
583+ /// NSIS installer synchronously (`/P /R`), so the broker must be gone BEFORE
584+ /// that call.
585+ ///
586+ /// Unlike the app-quit [`Self::shutdown_vss_helper`] (a bare `shutdown()`),
587+ /// this uses `set_enabled(false)`, which is a SUPERSET: it performs the same
588+ /// Shutdown+reap (including abandoning + reaping a `Pending` launch per the
589+ /// #113 generation semantics) AND disables the manager so a still-running
590+ /// sync that hits a locked file cannot RE-LAUNCH the elevated broker (and
591+ /// re-lock the exe) during the potentially-long `download_and_install`
592+ /// window. A memoised session decline is reset to `NotAttempted` by the
593+ /// underlying shutdown; that is inherent to any shutdown-based sweep and
594+ /// harmless here (an update is user-consented and a successful install
595+ /// restarts the app anyway).
596+ ///
597+ /// Best-effort + idempotent: a no-op (returns `false`) when no manager is in
598+ /// play (off Windows / elevated / setting off).
599+ pub fn shutdown_vss_helper_for_update ( & self ) -> bool {
600+ if let Some ( manager) = self . vss_helper_manager ( ) {
601+ manager. set_enabled ( false ) ;
602+ true
603+ } else {
604+ false
605+ }
606+ }
607+
608+ /// Re-arm the VSS helper broker after a FAILED update install (issue #125):
609+ /// the app keeps running, so undo the
610+ /// [`Self::shutdown_vss_helper_for_update`] disable so locked-file backup is
611+ /// available again on demand (a LAZY re-launch on the next locked file - no
612+ /// forced UAC prompt), rather than staying silently degraded until the next
613+ /// app restart. Best-effort + idempotent; a no-op when no manager is in play.
614+ pub fn rearm_vss_helper_after_failed_update ( & self ) {
615+ if let Some ( manager) = self . vss_helper_manager ( ) {
616+ manager. set_enabled ( true ) ;
617+ }
618+ }
619+
573620 // --- M9c D4: recovery-phrase ACK gate (M6 R4-P1-1, DATA-SAFETY) ---------
574621
575622 /// Lock the recovery-ack map, recovering a poisoned lock (house rule: never
@@ -1659,6 +1706,59 @@ mod tests {
16591706 let _ = std:: fs:: remove_dir_all ( dir) ;
16601707 }
16611708
1709+ #[ tokio:: test]
1710+ async fn shutdown_vss_helper_for_update_disables_then_rearm_restores ( ) {
1711+ // Issue #125: the updater path disables the broker BEFORE
1712+ // `download_and_install` so a live/relaunching elevated helper cannot hold
1713+ // its own exe open while the NSIS installer overwrites it. On a FAILED
1714+ // install (app keeps running) the caller re-arms it so locked-file backup
1715+ // is not left silently degraded.
1716+ use driven_vss_helper:: HelperLauncher ; // brings `is_available` into scope
1717+ let ( state, dir) = temp_state ( ) . await ;
1718+ let app_state = AppState :: new (
1719+ state,
1720+ HashMap :: new ( ) ,
1721+ RemoteMode :: Fake ,
1722+ default_fake_registry ( ) ,
1723+ ) ;
1724+
1725+ // No manager: both calls are safe no-ops; the pre-install sweep reports it
1726+ // disabled nothing.
1727+ assert ! ( !app_state. shutdown_vss_helper_for_update( ) ) ;
1728+ app_state. rearm_vss_helper_after_failed_update ( ) ;
1729+
1730+ // Install an ENABLED manager with an injected launch (no real UAC /
1731+ // process). Not launched yet -> NotAttempted + enabled == launchable.
1732+ let manager = Arc :: new ( crate :: vss_helper:: VssHelperManager :: with_launch_fn (
1733+ std:: env:: temp_dir ( ) . join ( "driven-vss-helper.exe" ) ,
1734+ std:: env:: temp_dir ( ) ,
1735+ true ,
1736+ Box :: new ( || Ok ( ( ) ) ) ,
1737+ ) ) ;
1738+ app_state. set_vss_helper_manager ( manager. clone ( ) ) ;
1739+ assert ! (
1740+ manager. is_available( ) ,
1741+ "an enabled, not-yet-tried broker is available on demand"
1742+ ) ;
1743+
1744+ // The pre-install sweep disables it (so a mid-download locked file cannot
1745+ // re-launch the broker) and reports it acted.
1746+ assert ! ( app_state. shutdown_vss_helper_for_update( ) ) ;
1747+ assert ! (
1748+ !manager. is_available( ) ,
1749+ "disabled broker is not available -> cannot re-launch during the install"
1750+ ) ;
1751+
1752+ // A failed install re-arms it so backup is available again on demand.
1753+ app_state. rearm_vss_helper_after_failed_update ( ) ;
1754+ assert ! (
1755+ manager. is_available( ) ,
1756+ "re-arm after a failed install restores on-demand launchability"
1757+ ) ;
1758+
1759+ let _ = std:: fs:: remove_dir_all ( dir) ;
1760+ }
1761+
16621762 #[ tokio:: test]
16631763 async fn recovery_ack_gate_requires_a_recorded_backend_reveal ( ) {
16641764 // M9c D4 (M6 R4-P1-1, DATA-SAFETY): the ack gate `ack_recovery_phrase_saved`
0 commit comments