Skip to main content

restart_manager/
session.rs

1//! Role-aware session lifecycle and owned recovery typestates.
2
3use std::borrow::Borrow;
4use std::ffi::OsStr;
5use std::fmt;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8use std::sync::{Arc, Weak};
9
10use crate::application::{
11    AffectedApplication, AffectedApplications, ApplicationStatus, ApplicationType, ProcessIdentity,
12    RebootReasons,
13};
14use crate::error::{Error, ErrorKind, ParseSessionKeyError, Result};
15use crate::filter::{Filter, FilterAction, FilterTarget};
16use crate::input::{absolute_user_path, validate_user_os_value};
17use crate::resource::ResourceBatch;
18use crate::shutdown::{OperationOutcome, Progress, RecoveryOutcome, ShutdownOptions};
19use crate::sys::{
20    self, RawAffectedApplications, RawFilter, RawFilterTarget, RawUniqueProcess, SessionHandle,
21    SysError,
22};
23
24const INVALID_NATIVE_ID: u32 = u32::MAX;
25const ERROR_FILE_NOT_FOUND: u32 = 2;
26const ERROR_ACCESS_DENIED: u32 = 5;
27const ERROR_INVALID_HANDLE: u32 = 6;
28const ERROR_OUTOFMEMORY: u32 = 14;
29const ERROR_WRITE_FAULT: u32 = 29;
30const ERROR_SEM_TIMEOUT: u32 = 121;
31const ERROR_BAD_ARGUMENTS: u32 = 160;
32const ERROR_DIRECTORY: u32 = 267;
33const ERROR_FAIL_NOACTION_REBOOT: u32 = 350;
34const ERROR_FAIL_SHUTDOWN: u32 = 351;
35const ERROR_FAIL_RESTART: u32 = 352;
36const ERROR_MAX_SESSIONS_REACHED: u32 = 353;
37const ERROR_REQUEST_OUT_OF_SEQUENCE: u32 = 776;
38const ERROR_SESSION_CREDENTIAL_CONFLICT: u32 = 1219;
39const ERROR_CANCELLED: u32 = 1223;
40
41/// A validated, 32-character ASCII hexadecimal Restart Manager session key.
42#[derive(Clone, PartialEq, Eq, Hash)]
43pub struct SessionKey(String);
44
45impl SessionKey {
46    /// Returns the key for explicit cross-process transfer.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51
52    /// Converts the key into its owned string.
53    #[must_use]
54    pub fn into_string(self) -> String {
55        self.0
56    }
57
58    fn generated(value: String) -> Result<Self> {
59        if is_valid_key(&value) {
60            Ok(Self(value))
61        } else {
62            Err(Error::new(
63                ErrorKind::MalformedOsData,
64                None,
65                "Windows returned a malformed Restart Manager session key",
66            ))
67        }
68    }
69}
70
71impl fmt::Debug for SessionKey {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        formatter
74            .debug_tuple("SessionKey")
75            .field(&"<redacted>")
76            .finish()
77    }
78}
79
80impl FromStr for SessionKey {
81    type Err = ParseSessionKeyError;
82
83    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
84        is_valid_key(value)
85            .then(|| Self(value.to_owned()))
86            .ok_or(ParseSessionKeyError)
87    }
88}
89
90fn is_valid_key(value: &str) -> bool {
91    value.len() == 32 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
92}
93
94struct SessionCore {
95    handle: Arc<SessionHandle>,
96    key: SessionKey,
97}
98
99impl SessionCore {
100    fn start() -> Result<Self> {
101        let (handle, key) = SessionHandle::start().map_err(map_sys_error)?;
102        Ok(Self {
103            handle: Arc::new(handle),
104            key: SessionKey::generated(key)?,
105        })
106    }
107
108    fn join(key: &SessionKey) -> Result<Self> {
109        let handle = SessionHandle::join(key.as_str()).map_err(map_join_error)?;
110        Ok(Self {
111            handle: Arc::new(handle),
112            key: key.clone(),
113        })
114    }
115
116    fn register_resources(&self, resources: &ResourceBatch) -> Result<()> {
117        if resources.is_empty() {
118            return Ok(());
119        }
120        let files = resources
121            .files()
122            .map(validate_and_absolute_file)
123            .collect::<Result<Vec<_>>>()?;
124        let processes = resources
125            .processes()
126            .iter()
127            .copied()
128            .map(raw_process)
129            .collect::<Vec<_>>();
130        let services = resources
131            .services()
132            .map(|name| {
133                validate_user_os_value(name, "a service short name")?;
134                Ok(name.to_os_string())
135            })
136            .collect::<Result<Vec<_>>>()?;
137        self.handle
138            .register_resources(&files, &processes, &services)
139            .map_err(|error| map_operation_error(error, Operation::Register))
140    }
141
142    fn affected_applications(&self) -> Result<AffectedApplications> {
143        self.handle
144            .affected_applications()
145            .map_err(|error| map_operation_error(error, Operation::Report))
146            .map(application_report)
147    }
148
149    fn end(self) -> Result<()> {
150        self.handle
151            .end()
152            .map_err(|error| map_operation_error(error, Operation::End))
153    }
154}
155
156/// A primary-installer Restart Manager session.
157///
158/// Shutdown consumes this value and produces a [RestartPending], so restart
159/// cannot be called out of sequence. This typestate is [`Send`].
160pub struct RestartSession {
161    core: SessionCore,
162}
163
164impl RestartSession {
165    /// Starts a new primary-installer session.
166    pub fn new() -> Result<Self> {
167        SessionCore::start().map(|core| Self { core })
168    }
169
170    /// Returns the key a secondary installer can pass to [JoinedSession::join].
171    #[must_use]
172    pub fn session_key(&self) -> &SessionKey {
173        &self.core.key
174    }
175
176    /// Registers a mixed collection in one native call.
177    pub fn register_resources(&mut self, resources: &ResourceBatch) -> Result<()> {
178        self.core.register_resources(resources)
179    }
180
181    /// Registers file paths. Directories are rejected by Restart Manager.
182    pub fn register_files<I, P>(&mut self, files: I) -> Result<()>
183    where
184        I: IntoIterator<Item = P>,
185        P: AsRef<Path>,
186    {
187        let mut resources = ResourceBatch::new();
188        for file in files {
189            resources.add_file(file.as_ref().to_path_buf());
190        }
191        self.core.register_resources(&resources)
192    }
193
194    /// Registers exact process identities from any iterator.
195    pub fn register_processes<I, P>(&mut self, processes: I) -> Result<()>
196    where
197        I: IntoIterator<Item = P>,
198        P: Borrow<ProcessIdentity>,
199    {
200        let mut resources = ResourceBatch::new();
201        for process in processes {
202            resources.add_process(*process.borrow());
203        }
204        self.core.register_resources(&resources)
205    }
206
207    /// Registers Windows services by short name.
208    pub fn register_services<I, S>(&mut self, services: I) -> Result<()>
209    where
210        I: IntoIterator<Item = S>,
211        S: AsRef<OsStr>,
212    {
213        let mut resources = ResourceBatch::new();
214        for service in services {
215            resources.add_service(service.as_ref().to_os_string());
216        }
217        self.core.register_resources(&resources)
218    }
219
220    /// Takes a reusable snapshot of affected applications and reboot reasons.
221    pub fn affected_applications(&mut self) -> Result<AffectedApplications> {
222        self.core.affected_applications()
223    }
224
225    /// Creates a weak, thread-safe cancellation capability.
226    #[must_use]
227    pub fn cancellation_handle(&self) -> CancellationHandle {
228        CancellationHandle {
229            handle: Arc::downgrade(&self.core.handle),
230        }
231    }
232
233    /// Gracefully attempts shutdown and always enters the recovery-required state.
234    pub fn shutdown(self) -> RestartPending {
235        self.shutdown_with_options(ShutdownOptions::default())
236    }
237
238    /// Attempts shutdown and retains its outcome regardless of native success.
239    pub fn shutdown_with_options(self, options: ShutdownOptions) -> RestartPending {
240        let mut pending = RestartPending::new(self.core);
241        let result = pending
242            .core()
243            .handle
244            .shutdown(options.native_flags())
245            .map_err(|error| map_operation_error(error, Operation::Shutdown));
246        pending.shutdown = Some(OperationOutcome::from_result(result));
247        pending
248    }
249
250    /// Attempts shutdown while delivering validated, strictly increasing progress.
251    ///
252    /// If the process-global callback lease cannot be acquired, the returned
253    /// error also returns this session and no native operation has started.
254    pub fn shutdown_with_progress<F>(
255        self,
256        options: ShutdownOptions,
257        mut callback: F,
258    ) -> std::result::Result<RestartPending, OperationNotStarted<Self>>
259    where
260        F: FnMut(Progress) + Send,
261    {
262        let mut pending = RestartPending::new(self.core);
263        let mut last = None;
264        let mut malformed = false;
265        let result = pending
266            .core()
267            .handle
268            .shutdown_with_progress(options.native_flags(), &mut |native| {
269                deliver_progress(native, &mut last, &mut malformed, &mut callback)
270            });
271        if let Err(error) = result {
272            if operation_not_started(error) {
273                let state = Self {
274                    core: pending.take_core(),
275                };
276                return Err(OperationNotStarted::new(state, map_sys_error(error)));
277            }
278            pending.shutdown = Some(OperationOutcome::Failed(map_operation_error(
279                error,
280                Operation::Shutdown,
281            )));
282            return Ok(pending);
283        }
284        pending.shutdown = Some(if malformed {
285            OperationOutcome::Failed(malformed_progress_error())
286        } else {
287            OperationOutcome::Succeeded
288        });
289        Ok(pending)
290    }
291
292    /// Adds or replaces a restart/shutdown filter.
293    pub fn set_filter(&mut self, target: &FilterTarget, action: FilterAction) -> Result<()> {
294        self.core
295            .handle
296            .add_filter(&raw_filter_target(target), raw_filter_action(action))
297            .map_err(|error| map_operation_error(error, Operation::SetFilter))
298    }
299
300    /// Removes a filter from the selected target.
301    pub fn remove_filter(&mut self, target: &FilterTarget) -> Result<()> {
302        self.core
303            .handle
304            .remove_filter(&raw_filter_target(target))
305            .map_err(|error| map_operation_error(error, Operation::RemoveFilter))
306    }
307
308    /// Lists filters configured by this primary installer.
309    pub fn filters(&mut self) -> Result<Vec<Filter>> {
310        self.core
311            .handle
312            .filters()
313            .map_err(|error| map_operation_error(error, Operation::Filters))?
314            .into_iter()
315            .map(filter_from_raw)
316            .collect()
317    }
318
319    /// Ends the session without attempting shutdown.
320    pub fn end(self) -> Result<()> {
321        self.core.end()
322    }
323}
324
325/// The state entered after any shutdown attempt.
326///
327/// Dropping an armed value makes one best-effort restart attempt and then ends
328/// the session. This covers early returns, panics, and partial shutdown. This
329/// typestate is [`Send`].
330#[must_use = "dropping this value attempts recovery; call restart or leave_stopped explicitly"]
331pub struct RestartPending {
332    core: Option<SessionCore>,
333    shutdown: Option<OperationOutcome>,
334}
335
336impl RestartPending {
337    fn new(core: SessionCore) -> Self {
338        Self {
339            core: Some(core),
340            shutdown: Some(OperationOutcome::Succeeded),
341        }
342    }
343
344    fn core(&self) -> &SessionCore {
345        self.core.as_ref().expect("pending session owns its core")
346    }
347
348    fn take_core(&mut self) -> SessionCore {
349        self.core.take().expect("pending session owns its core")
350    }
351
352    fn take_shutdown(&mut self) -> OperationOutcome {
353        self.shutdown
354            .take()
355            .expect("pending session owns its shutdown outcome")
356    }
357
358    /// Returns the retained shutdown result.
359    #[must_use]
360    pub const fn shutdown_outcome(&self) -> &OperationOutcome {
361        match self.shutdown.as_ref() {
362            Some(outcome) => outcome,
363            None => panic!("pending session owns its shutdown outcome"),
364        }
365    }
366
367    /// Attempts restart even when shutdown failed.
368    #[must_use]
369    pub fn restart(mut self) -> RecoveryCompletion {
370        let result = self
371            .core()
372            .handle
373            .restart()
374            .map_err(|error| map_operation_error(error, Operation::Restart));
375        let outcome = RecoveryOutcome {
376            shutdown: self.take_shutdown(),
377            restart: Some(OperationOutcome::from_result(result)),
378        };
379        RecoveryCompletion {
380            core: self.take_core(),
381            outcome,
382        }
383    }
384
385    /// Attempts restart with validated, strictly increasing progress.
386    ///
387    /// The error deliberately owns the full pending state so recovery cannot
388    /// be lost merely to reduce the enum size.
389    #[allow(clippy::result_large_err)]
390    pub fn restart_with_progress<F>(
391        mut self,
392        mut callback: F,
393    ) -> std::result::Result<RecoveryCompletion, OperationNotStarted<Self>>
394    where
395        F: FnMut(Progress) + Send,
396    {
397        let mut last = None;
398        let mut malformed = false;
399        let result = self.core().handle.restart_with_progress(&mut |native| {
400            deliver_progress(native, &mut last, &mut malformed, &mut callback);
401        });
402        if let Err(error) = result {
403            if operation_not_started(error) {
404                return Err(OperationNotStarted::new(self, map_sys_error(error)));
405            }
406            let outcome = RecoveryOutcome {
407                shutdown: self.take_shutdown(),
408                restart: Some(OperationOutcome::Failed(map_operation_error(
409                    error,
410                    Operation::Restart,
411                ))),
412            };
413            return Ok(RecoveryCompletion {
414                core: self.take_core(),
415                outcome,
416            });
417        }
418        let restart = if malformed {
419            OperationOutcome::Failed(malformed_progress_error())
420        } else {
421            OperationOutcome::Succeeded
422        };
423        let outcome = RecoveryOutcome {
424            shutdown: self.take_shutdown(),
425            restart: Some(restart),
426        };
427        Ok(RecoveryCompletion {
428            core: self.take_core(),
429            outcome,
430        })
431    }
432
433    /// Explicitly opts out of automatic restart.
434    #[must_use]
435    pub fn leave_stopped(mut self) -> RecoveryCompletion {
436        let outcome = RecoveryOutcome {
437            shutdown: self.take_shutdown(),
438            restart: None,
439        };
440        RecoveryCompletion {
441            core: self.take_core(),
442            outcome,
443        }
444    }
445}
446
447impl Drop for RestartPending {
448    fn drop(&mut self) {
449        if let Some(core) = self.core.take() {
450            let _ = core.handle.restart();
451            drop(core);
452        }
453    }
454}
455
456/// Completed recovery state with retained results and post-operation reporting.
457/// This typestate is [`Send`].
458pub struct RecoveryCompletion {
459    core: SessionCore,
460    outcome: RecoveryOutcome,
461}
462
463impl RecoveryCompletion {
464    /// Returns both retained operation outcomes.
465    #[must_use]
466    pub const fn outcome(&self) -> &RecoveryOutcome {
467        &self.outcome
468    }
469
470    /// Takes a post-operation affected-application snapshot.
471    pub fn affected_applications(&mut self) -> Result<AffectedApplications> {
472        self.core.affected_applications()
473    }
474
475    /// Ends the session and returns both retained operation outcomes.
476    ///
477    /// If ending fails, destruction retries the native end once.
478    pub fn end(self) -> Result<RecoveryOutcome> {
479        let outcome = self.outcome;
480        self.core.end()?;
481        Ok(outcome)
482    }
483}
484
485/// A consuming operation that failed before native work began.
486pub struct OperationNotStarted<T> {
487    state: T,
488    error: Error,
489}
490
491impl<T> OperationNotStarted<T> {
492    pub(crate) fn new(state: T, error: Error) -> Self {
493        Self { state, error }
494    }
495
496    /// Returns the error.
497    #[must_use]
498    pub const fn error(&self) -> &Error {
499        &self.error
500    }
501
502    /// Returns the original state.
503    #[must_use]
504    pub const fn state(&self) -> &T {
505        &self.state
506    }
507
508    /// Separates the original state and error.
509    #[must_use]
510    pub fn into_parts(self) -> (T, Error) {
511        (self.state, self.error)
512    }
513}
514
515impl<T> fmt::Debug for OperationNotStarted<T> {
516    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
517        formatter
518            .debug_struct("OperationNotStarted")
519            .field("state", &std::any::type_name::<T>())
520            .field("error", &self.error)
521            .finish()
522    }
523}
524
525impl<T> fmt::Display for OperationNotStarted<T> {
526    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
527        self.error.fmt(formatter)
528    }
529}
530
531impl<T> std::error::Error for OperationNotStarted<T> {
532    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
533        Some(&self.error)
534    }
535}
536
537/// A secondary-installer view of an existing session.
538///
539/// This role can only register resources, inspect its key, and end. This
540/// typestate is [`Send`].
541pub struct JoinedSession {
542    core: SessionCore,
543}
544
545impl JoinedSession {
546    /// Joins a session created by a primary installer.
547    pub fn join(key: &SessionKey) -> Result<Self> {
548        SessionCore::join(key).map(|core| Self { core })
549    }
550
551    /// Returns the key used by this joined session.
552    #[must_use]
553    pub fn session_key(&self) -> &SessionKey {
554        &self.core.key
555    }
556
557    /// Registers a mixed collection in one native call.
558    pub fn register_resources(&mut self, resources: &ResourceBatch) -> Result<()> {
559        self.core.register_resources(resources)
560    }
561
562    /// Registers file paths.
563    pub fn register_files<I, P>(&mut self, files: I) -> Result<()>
564    where
565        I: IntoIterator<Item = P>,
566        P: AsRef<Path>,
567    {
568        let mut resources = ResourceBatch::new();
569        for file in files {
570            resources.add_file(file.as_ref().to_path_buf());
571        }
572        self.core.register_resources(&resources)
573    }
574
575    /// Registers exact process identities from any iterator.
576    pub fn register_processes<I, P>(&mut self, processes: I) -> Result<()>
577    where
578        I: IntoIterator<Item = P>,
579        P: Borrow<ProcessIdentity>,
580    {
581        let mut resources = ResourceBatch::new();
582        for process in processes {
583            resources.add_process(*process.borrow());
584        }
585        self.core.register_resources(&resources)
586    }
587
588    /// Registers Windows services by short name.
589    pub fn register_services<I, S>(&mut self, services: I) -> Result<()>
590    where
591        I: IntoIterator<Item = S>,
592        S: AsRef<OsStr>,
593    {
594        let mut resources = ResourceBatch::new();
595        for service in services {
596            resources.add_service(service.as_ref().to_os_string());
597        }
598        self.core.register_resources(&resources)
599    }
600
601    /// Ends the joined handle.
602    pub fn end(self) -> Result<()> {
603        self.core.end()
604    }
605}
606
607/// A weak, [`Send`] + [`Sync`] capability that can cancel a blocking native
608/// operation.
609#[derive(Clone)]
610pub struct CancellationHandle {
611    handle: Weak<SessionHandle>,
612}
613
614impl fmt::Debug for CancellationHandle {
615    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
616        formatter
617            .debug_struct("CancellationHandle")
618            .finish_non_exhaustive()
619    }
620}
621
622impl CancellationHandle {
623    /// Requests cancellation without keeping an ended session alive.
624    pub fn cancel(&self) -> Result<()> {
625        let handle = self.handle.upgrade().ok_or_else(|| {
626            Error::new(
627                ErrorKind::SessionEnded,
628                None,
629                "the Restart Manager session has already ended",
630            )
631        })?;
632        handle.cancel().map_err(map_sys_error)
633    }
634}
635
636impl ProcessIdentity {
637    /// Looks up a process creation time and builds a non-recyclable identity.
638    pub fn from_pid(pid: u32) -> Result<Self> {
639        if pid == 0 || pid == INVALID_NATIVE_ID {
640            return Self::from_raw_parts(pid, 0);
641        }
642        sys::process_from_pid(pid)
643            .map(process_from_raw)
644            .map_err(map_sys_error)
645    }
646
647    /// Returns the current process identity.
648    pub fn current() -> Result<Self> {
649        sys::current_process()
650            .map(process_from_raw)
651            .map_err(map_sys_error)
652    }
653}
654
655fn deliver_progress<F>(
656    native: u32,
657    last: &mut Option<Progress>,
658    malformed: &mut bool,
659    callback: &mut F,
660) where
661    F: FnMut(Progress),
662{
663    if *malformed {
664        return;
665    }
666    let Some(progress) = Progress::try_from_native(native) else {
667        *malformed = true;
668        return;
669    };
670    if last.is_some_and(|previous| progress <= previous) {
671        *malformed = true;
672        return;
673    }
674    *last = Some(progress);
675    callback(progress);
676}
677
678fn malformed_progress_error() -> Error {
679    Error::new(
680        ErrorKind::MalformedOsData,
681        None,
682        "Windows reported progress outside 0..=100 or not strictly increasing",
683    )
684}
685
686fn operation_not_started(error: SysError) -> bool {
687    matches!(
688        error,
689        SysError::CallbackInUse | SysError::UnsupportedPlatform
690    )
691}
692
693fn validate_and_absolute_file(path: &Path) -> Result<PathBuf> {
694    let path = absolute_user_path(path, "a file path")?;
695    if path.is_dir() {
696        return Err(Error::new(
697            ErrorKind::DirectoryNotSupported,
698            None,
699            "Restart Manager does not support directory resources",
700        ));
701    }
702    Ok(path)
703}
704
705fn application_report(raw: RawAffectedApplications) -> AffectedApplications {
706    let applications = raw
707        .applications
708        .into_iter()
709        .map(|application| AffectedApplication {
710            display_name: application.display_name,
711            service_name: (!application.service_name.is_empty())
712                .then_some(application.service_name),
713            application_type: ApplicationType::from_raw(application.application_type),
714            status: ApplicationStatus::from_bits_retain(application.status),
715            restartable: application.restartable,
716            process: (application.process.pid != 0 && application.process.pid != INVALID_NATIVE_ID)
717                .then(|| process_from_raw(application.process)),
718            terminal_session_id: (application.terminal_session_id != INVALID_NATIVE_ID)
719                .then_some(application.terminal_session_id),
720        })
721        .collect();
722    AffectedApplications {
723        applications,
724        reboot_reasons: RebootReasons::from_bits_retain(raw.reboot_reasons),
725    }
726}
727
728fn raw_process(process: ProcessIdentity) -> RawUniqueProcess {
729    RawUniqueProcess {
730        pid: process.pid(),
731        start_time: process.creation_time_100ns_since_1601(),
732    }
733}
734
735fn process_from_raw(process: RawUniqueProcess) -> ProcessIdentity {
736    ProcessIdentity::from_raw_parts_unchecked(process.pid, process.start_time)
737}
738
739fn raw_filter_target(target: &FilterTarget) -> RawFilterTarget {
740    if let Some(path) = target.as_executable() {
741        RawFilterTarget::Executable(path.to_path_buf())
742    } else if let Some(process) = target.as_process() {
743        RawFilterTarget::Process(raw_process(process))
744    } else if let Some(service) = target.as_service() {
745        RawFilterTarget::Service(service.to_os_string())
746    } else {
747        unreachable!("validated filter targets have exactly one identity")
748    }
749}
750
751fn raw_filter_action(action: FilterAction) -> i32 {
752    match action {
753        FilterAction::PreventRestart => 1,
754        FilterAction::PreventShutdown => 2,
755    }
756}
757
758fn filter_from_raw(filter: RawFilter) -> Result<Filter> {
759    let target = match filter.target {
760        RawFilterTarget::Executable(path) => FilterTarget::from_raw_executable(path)?,
761        RawFilterTarget::Process(process) => {
762            if process.pid == 0 || process.pid == INVALID_NATIVE_ID {
763                return Err(Error::new(
764                    ErrorKind::MalformedOsData,
765                    None,
766                    "Windows returned an invalid process filter identity",
767                ));
768            }
769            FilterTarget::from_raw_process(process_from_raw(process))
770        }
771        RawFilterTarget::Service(service) => FilterTarget::from_raw_service(service)?,
772    };
773    let action = match filter.action {
774        1 => FilterAction::PreventRestart,
775        2 => FilterAction::PreventShutdown,
776        _ => {
777            return Err(Error::new(
778                ErrorKind::MalformedOsData,
779                None,
780                "Windows returned an unknown filter action",
781            ));
782        }
783    };
784    Ok(Filter { target, action })
785}
786
787#[derive(Clone, Copy)]
788enum Operation {
789    Register,
790    Report,
791    Shutdown,
792    Restart,
793    SetFilter,
794    RemoveFilter,
795    Filters,
796    End,
797}
798
799fn map_join_error(error: SysError) -> Error {
800    match error {
801        SysError::Os(
802            ERROR_INVALID_HANDLE | ERROR_BAD_ARGUMENTS | ERROR_SESSION_CREDENTIAL_CONFLICT,
803        ) => Error::new(
804            ErrorKind::InvalidSessionKey,
805            error.raw_os_error(),
806            "the Restart Manager session key was rejected",
807        ),
808        other => map_sys_error(other),
809    }
810}
811
812fn map_operation_error(error: SysError, operation: Operation) -> Error {
813    match (operation, error) {
814        (Operation::Register | Operation::Report, SysError::Os(ERROR_DIRECTORY)) => Error::new(
815            ErrorKind::DirectoryNotSupported,
816            Some(ERROR_DIRECTORY),
817            "Restart Manager does not support directory resources",
818        ),
819        (Operation::Shutdown, SysError::Os(ERROR_FAIL_NOACTION_REBOOT)) => Error::new(
820            ErrorKind::RebootRequired,
821            Some(ERROR_FAIL_NOACTION_REBOOT),
822            "a system reboot is required before applications can be shut down",
823        ),
824        (Operation::Shutdown, SysError::Os(ERROR_FAIL_SHUTDOWN)) => Error::new(
825            ErrorKind::ShutdownIncomplete,
826            Some(ERROR_FAIL_SHUTDOWN),
827            "one or more affected applications could not be shut down",
828        ),
829        (Operation::Restart, SysError::Os(ERROR_FAIL_RESTART)) => Error::new(
830            ErrorKind::RestartIncomplete,
831            Some(ERROR_FAIL_RESTART),
832            "one or more stopped applications could not be restarted",
833        ),
834        (Operation::Restart, SysError::Os(ERROR_REQUEST_OUT_OF_SEQUENCE)) => Error::new(
835            ErrorKind::OperationOutOfSequence,
836            Some(ERROR_REQUEST_OUT_OF_SEQUENCE),
837            "Restart Manager rejected an out-of-sequence restart",
838        ),
839        (Operation::RemoveFilter, SysError::Os(ERROR_FILE_NOT_FOUND)) => Error::new(
840            ErrorKind::FilterNotFound,
841            Some(ERROR_FILE_NOT_FOUND),
842            "the selected Restart Manager filter does not exist",
843        ),
844        (_, other) => map_sys_error(other),
845    }
846}
847
848pub(crate) fn map_sys_error(error: SysError) -> Error {
849    match error {
850        SysError::UnsupportedPlatform => Error::new(
851            ErrorKind::UnsupportedPlatform,
852            None,
853            "Windows Restart Manager is unavailable on this platform",
854        ),
855        SysError::Os(ERROR_MAX_SESSIONS_REACHED) => Error::new(
856            ErrorKind::SessionLimit,
857            Some(ERROR_MAX_SESSIONS_REACHED),
858            "the limit of 64 concurrent Restart Manager sessions was reached",
859        ),
860        SysError::Os(ERROR_CANCELLED) => Error::new(
861            ErrorKind::Cancelled,
862            Some(ERROR_CANCELLED),
863            "the Restart Manager operation was cancelled",
864        ),
865        SysError::Os(ERROR_ACCESS_DENIED) => Error::new(
866            ErrorKind::AccessDenied,
867            Some(ERROR_ACCESS_DENIED),
868            "Windows denied access to the requested resource",
869        ),
870        SysError::Os(ERROR_BAD_ARGUMENTS) => Error::new(
871            ErrorKind::InvalidInput,
872            Some(ERROR_BAD_ARGUMENTS),
873            "Restart Manager rejected an invalid argument",
874        ),
875        SysError::Os(ERROR_INVALID_HANDLE) => Error::new(
876            ErrorKind::SessionEnded,
877            Some(ERROR_INVALID_HANDLE),
878            "the Restart Manager session handle is no longer valid",
879        ),
880        SysError::Os(ERROR_OUTOFMEMORY) => Error::new(
881            ErrorKind::OutOfMemory,
882            Some(ERROR_OUTOFMEMORY),
883            "Restart Manager could not allocate required memory",
884        ),
885        SysError::Os(ERROR_SEM_TIMEOUT | ERROR_WRITE_FAULT) => Error::new(
886            ErrorKind::RegistryUnavailable,
887            error.raw_os_error(),
888            "Restart Manager could not access its registry state",
889        ),
890        SysError::Os(code) => Error::new(
891            ErrorKind::Os,
892            Some(code),
893            format!("Restart Manager call failed (Win32 error {code})"),
894        ),
895        SysError::HResult(code) => {
896            let kind = match code as u32 {
897                0x8007_0057 => ErrorKind::InvalidInput,
898                0x8007_000E => ErrorKind::OutOfMemory,
899                _ => ErrorKind::Os,
900            };
901            Error::from_hresult(
902                kind,
903                code,
904                format!(
905                    "application restart call failed (HRESULT {:#010x})",
906                    code as u32
907                ),
908            )
909        }
910        SysError::InvalidInput(detail) => Error::new(ErrorKind::InvalidInput, None, detail),
911        SysError::CountOverflow => Error::new(
912            ErrorKind::TooManyResources,
913            None,
914            "a native Restart Manager count would overflow",
915        ),
916        SysError::AllocationFailure => Error::new(
917            ErrorKind::OutOfMemory,
918            None,
919            "memory allocation for a native result failed",
920        ),
921        SysError::CallbackInUse => Error::new(
922            ErrorKind::CallbackInUse,
923            None,
924            "another progress callback operation is active in this process",
925        ),
926        SysError::DataChanged(code) => Error::new(
927            ErrorKind::DataChanged,
928            Some(code),
929            "the Restart Manager list kept changing through all eight retries",
930        ),
931        SysError::MalformedOutput(detail) => Error::new(ErrorKind::MalformedOsData, None, detail),
932        SysError::SessionEnded => Error::new(
933            ErrorKind::SessionEnded,
934            None,
935            "the Restart Manager session has already ended",
936        ),
937    }
938}
939
940#[cfg(test)]
941mod tests {
942    use super::*;
943    use std::ffi::OsString;
944
945    #[test]
946    fn key_parse_is_dedicated_and_debug_is_redacted() {
947        let key: SessionKey = "0123456789abcdefABCDEF0123456789".parse().unwrap();
948        assert_eq!(key.as_str(), "0123456789abcdefABCDEF0123456789");
949        assert_eq!(format!("{key:?}"), "SessionKey(\"<redacted>\")");
950        assert!("short".parse::<SessionKey>().is_err());
951        assert_eq!(
952            key.clone().into_string(),
953            "0123456789abcdefABCDEF0123456789"
954        );
955        assert_eq!(
956            SessionKey::generated("invalid".to_owned())
957                .unwrap_err()
958                .kind(),
959            ErrorKind::MalformedOsData
960        );
961    }
962
963    #[test]
964    fn operation_mapping_is_specific() {
965        assert_eq!(
966            map_operation_error(SysError::Os(351), Operation::Shutdown).kind(),
967            ErrorKind::ShutdownIncomplete
968        );
969        assert_eq!(
970            map_operation_error(SysError::Os(352), Operation::Restart).kind(),
971            ErrorKind::RestartIncomplete
972        );
973        assert_eq!(
974            map_operation_error(SysError::Os(267), Operation::Register).kind(),
975            ErrorKind::DirectoryNotSupported
976        );
977        assert_eq!(
978            map_operation_error(SysError::Os(350), Operation::Shutdown).kind(),
979            ErrorKind::RebootRequired
980        );
981        assert_eq!(
982            map_operation_error(SysError::Os(776), Operation::Restart).kind(),
983            ErrorKind::OperationOutOfSequence
984        );
985        assert_eq!(
986            map_operation_error(SysError::Os(2), Operation::RemoveFilter).kind(),
987            ErrorKind::FilterNotFound
988        );
989        assert_eq!(
990            map_sys_error(SysError::Os(121)).kind(),
991            ErrorKind::RegistryUnavailable
992        );
993        assert_eq!(
994            map_sys_error(SysError::Os(14)).kind(),
995            ErrorKind::OutOfMemory
996        );
997        assert_eq!(
998            map_sys_error(SysError::Os(160)).kind(),
999            ErrorKind::InvalidInput
1000        );
1001        let cases = [
1002            (
1003                SysError::UnsupportedPlatform,
1004                ErrorKind::UnsupportedPlatform,
1005            ),
1006            (SysError::Os(353), ErrorKind::SessionLimit),
1007            (SysError::Os(1223), ErrorKind::Cancelled),
1008            (SysError::Os(5), ErrorKind::AccessDenied),
1009            (SysError::Os(6), ErrorKind::SessionEnded),
1010            (SysError::Os(29), ErrorKind::RegistryUnavailable),
1011            (SysError::Os(999), ErrorKind::Os),
1012            (SysError::InvalidInput("bad"), ErrorKind::InvalidInput),
1013            (SysError::CountOverflow, ErrorKind::TooManyResources),
1014            (SysError::AllocationFailure, ErrorKind::OutOfMemory),
1015            (SysError::CallbackInUse, ErrorKind::CallbackInUse),
1016            (SysError::DataChanged(234), ErrorKind::DataChanged),
1017            (
1018                SysError::MalformedOutput("bad output"),
1019                ErrorKind::MalformedOsData,
1020            ),
1021            (SysError::SessionEnded, ErrorKind::SessionEnded),
1022        ];
1023        for (error, kind) in cases {
1024            assert_eq!(map_sys_error(error).kind(), kind);
1025        }
1026        for code in [
1027            ERROR_INVALID_HANDLE,
1028            ERROR_BAD_ARGUMENTS,
1029            ERROR_SESSION_CREDENTIAL_CONFLICT,
1030        ] {
1031            assert_eq!(
1032                map_join_error(SysError::Os(code)).kind(),
1033                ErrorKind::InvalidSessionKey
1034            );
1035        }
1036        assert_eq!(map_join_error(SysError::Os(999)).kind(), ErrorKind::Os);
1037    }
1038
1039    #[test]
1040    fn hresult_mapping_is_lossless() {
1041        let error = map_sys_error(SysError::HResult(0x8007_0057_u32 as i32));
1042        assert_eq!(error.kind(), ErrorKind::InvalidInput);
1043        assert_eq!(error.raw_hresult(), Some(0x8007_0057_u32 as i32));
1044        assert_eq!(error.raw_os_error(), None);
1045    }
1046
1047    #[test]
1048    fn progress_rejects_duplicate_decrease_and_range() {
1049        let mut last = None;
1050        let mut malformed = false;
1051        let mut values = Vec::new();
1052        deliver_progress(10, &mut last, &mut malformed, &mut |p| values.push(p));
1053        deliver_progress(10, &mut last, &mut malformed, &mut |p| values.push(p));
1054        deliver_progress(20, &mut last, &mut malformed, &mut |p| values.push(p));
1055        assert!(malformed);
1056        assert_eq!(values.len(), 1);
1057        deliver_progress(101, &mut last, &mut malformed, &mut |p| values.push(p));
1058        let mut fresh_last = None;
1059        let mut fresh_malformed = false;
1060        deliver_progress(101, &mut fresh_last, &mut fresh_malformed, &mut |_| {});
1061        assert!(fresh_malformed);
1062        assert_eq!(
1063            malformed_progress_error().kind(),
1064            ErrorKind::MalformedOsData
1065        );
1066        assert!(operation_not_started(SysError::UnsupportedPlatform));
1067        assert!(!operation_not_started(SysError::Os(5)));
1068    }
1069
1070    #[cfg(windows)]
1071    #[test]
1072    #[allow(clippy::result_large_err)]
1073    fn callback_lease_failure_returns_owned_state_before_shutdown_or_restart() {
1074        let session = RestartSession::new().unwrap();
1075        let result = sys::with_callback_lease_for_test(|| {
1076            session.shutdown_with_progress(ShutdownOptions::default(), |_| {})
1077        });
1078        let not_started = match result {
1079            Err(not_started) => not_started,
1080            Ok(_) => panic!("shutdown unexpectedly acquired the callback lease"),
1081        };
1082        assert_eq!(not_started.error().kind(), ErrorKind::CallbackInUse);
1083        assert!(not_started.state().session_key().as_str().len() == 32);
1084        assert!(format!("{not_started:?}").contains("OperationNotStarted"));
1085        assert_eq!(not_started.to_string(), not_started.error().to_string());
1086        assert!(std::error::Error::source(&not_started).is_some());
1087        let (session, _) = not_started.into_parts();
1088
1089        let pending = session.shutdown();
1090        let result = sys::with_callback_lease_for_test(|| pending.restart_with_progress(|_| {}));
1091        let not_started = match result {
1092            Err(not_started) => not_started,
1093            Ok(_) => panic!("restart unexpectedly acquired the callback lease"),
1094        };
1095        assert_eq!(not_started.error().kind(), ErrorKind::CallbackInUse);
1096        let (pending, _) = not_started.into_parts();
1097        pending.leave_stopped().end().unwrap();
1098    }
1099
1100    #[cfg(windows)]
1101    #[test]
1102    fn progress_paths_and_pending_drop_cover_recovery_lifecycle() {
1103        let _test_guard = sys::serialize_callback_test();
1104        let mut session = RestartSession::new().unwrap();
1105        let process = ProcessIdentity::current().unwrap();
1106        session.register_processes([process]).unwrap();
1107        session
1108            .set_filter(
1109                &FilterTarget::process(process),
1110                FilterAction::PreventShutdown,
1111            )
1112            .unwrap();
1113        let mut shutdown_progress = Vec::new();
1114        let pending = session
1115            .shutdown_with_progress(ShutdownOptions::default(), |progress| {
1116                shutdown_progress.push(progress);
1117            })
1118            .unwrap();
1119        assert!(!shutdown_progress.is_empty());
1120        let mut completion = pending.restart_with_progress(|_| {}).unwrap();
1121        completion.affected_applications().unwrap();
1122        completion.end().unwrap();
1123
1124        RestartSession::new()
1125            .unwrap()
1126            .shutdown()
1127            .leave_stopped()
1128            .end()
1129            .unwrap();
1130        drop(RestartSession::new().unwrap().shutdown());
1131    }
1132
1133    #[test]
1134    fn report_and_filter_raw_conversions_cover_every_variant() {
1135        let process = RawUniqueProcess {
1136            pid: 17,
1137            start_time: 23,
1138        };
1139        let report = application_report(RawAffectedApplications {
1140            applications: vec![sys::RawApplication {
1141                display_name: OsString::from("display"),
1142                service_name: OsString::from("service"),
1143                application_type: 3,
1144                status: 0x21,
1145                restartable: true,
1146                process,
1147                terminal_session_id: 4,
1148            }],
1149            reboot_reasons: 2,
1150        });
1151        assert_eq!(
1152            report.applications()[0].display_name(),
1153            OsStr::new("display")
1154        );
1155        assert_eq!(
1156            report.applications()[0].service_name(),
1157            Some(OsStr::new("service"))
1158        );
1159        assert_eq!(report.applications()[0].process().unwrap().pid(), 17);
1160        assert_eq!(report.applications()[0].terminal_session_id(), Some(4));
1161
1162        for target in [
1163            RawFilterTarget::Executable(std::path::absolute("absolute.exe").unwrap()),
1164            RawFilterTarget::Process(process),
1165            RawFilterTarget::Service(OsString::from("EventLog")),
1166        ] {
1167            let filter = filter_from_raw(RawFilter { target, action: 1 }).unwrap();
1168            assert_eq!(filter.action(), FilterAction::PreventRestart);
1169        }
1170        assert_eq!(
1171            filter_from_raw(RawFilter {
1172                target: RawFilterTarget::Service(OsString::from("EventLog")),
1173                action: 99,
1174            })
1175            .unwrap_err()
1176            .kind(),
1177            ErrorKind::MalformedOsData
1178        );
1179        for target in [
1180            RawFilterTarget::Executable(PathBuf::new()),
1181            RawFilterTarget::Executable(PathBuf::from("relative.exe")),
1182            RawFilterTarget::Executable({
1183                let mut path = std::env::current_dir().unwrap();
1184                path.push("bad\0filter.exe");
1185                path
1186            }),
1187            RawFilterTarget::Service(OsString::new()),
1188            RawFilterTarget::Service(OsString::from("bad\0service")),
1189        ] {
1190            assert_eq!(
1191                filter_from_raw(RawFilter { target, action: 1 })
1192                    .unwrap_err()
1193                    .kind(),
1194                ErrorKind::MalformedOsData
1195            );
1196        }
1197        assert_eq!(
1198            filter_from_raw(RawFilter {
1199                target: RawFilterTarget::Process(RawUniqueProcess {
1200                    pid: 0,
1201                    start_time: 0,
1202                }),
1203                action: 1,
1204            })
1205            .unwrap_err()
1206            .kind(),
1207            ErrorKind::MalformedOsData
1208        );
1209
1210        let report = application_report(RawAffectedApplications {
1211            applications: vec![sys::RawApplication {
1212                display_name: OsString::new(),
1213                service_name: OsString::new(),
1214                application_type: 0,
1215                status: 0,
1216                restartable: false,
1217                process: RawUniqueProcess {
1218                    pid: INVALID_NATIVE_ID,
1219                    start_time: 0,
1220                },
1221                terminal_session_id: INVALID_NATIVE_ID,
1222            }],
1223            reboot_reasons: 0,
1224        });
1225        assert!(report.applications()[0].service_name().is_none());
1226        assert!(report.applications()[0].process().is_none());
1227        assert!(report.applications()[0].terminal_session_id().is_none());
1228    }
1229
1230    #[cfg(windows)]
1231    #[test]
1232    fn public_input_validation_and_debug_paths_are_reached() {
1233        assert!(ProcessIdentity::from_pid(0).is_err());
1234        assert!(ProcessIdentity::from_pid(INVALID_NATIVE_ID).is_err());
1235
1236        let mut session = RestartSession::new().unwrap();
1237        assert_eq!(
1238            session.register_services([""]).unwrap_err().kind(),
1239            ErrorKind::InvalidInput
1240        );
1241        assert_eq!(
1242            session
1243                .register_services(["bad\0service"])
1244                .unwrap_err()
1245                .kind(),
1246            ErrorKind::InvalidInput
1247        );
1248        assert_eq!(
1249            session.register_files([""]).unwrap_err().kind(),
1250            ErrorKind::InvalidInput
1251        );
1252        let cancellation = session.cancellation_handle();
1253        assert!(format!("{cancellation:?}").contains("CancellationHandle"));
1254        session.end().unwrap();
1255        assert_eq!(
1256            cancellation.cancel().unwrap_err().kind(),
1257            ErrorKind::SessionEnded
1258        );
1259    }
1260}