1use 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#[derive(Clone, PartialEq, Eq, Hash)]
43pub struct SessionKey(String);
44
45impl SessionKey {
46 #[must_use]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51
52 #[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
156pub struct RestartSession {
161 core: SessionCore,
162}
163
164impl RestartSession {
165 pub fn new() -> Result<Self> {
167 SessionCore::start().map(|core| Self { core })
168 }
169
170 #[must_use]
172 pub fn session_key(&self) -> &SessionKey {
173 &self.core.key
174 }
175
176 pub fn register_resources(&mut self, resources: &ResourceBatch) -> Result<()> {
178 self.core.register_resources(resources)
179 }
180
181 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 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 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 pub fn affected_applications(&mut self) -> Result<AffectedApplications> {
222 self.core.affected_applications()
223 }
224
225 #[must_use]
227 pub fn cancellation_handle(&self) -> CancellationHandle {
228 CancellationHandle {
229 handle: Arc::downgrade(&self.core.handle),
230 }
231 }
232
233 pub fn shutdown(self) -> RestartPending {
235 self.shutdown_with_options(ShutdownOptions::default())
236 }
237
238 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 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 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 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 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 pub fn end(self) -> Result<()> {
321 self.core.end()
322 }
323}
324
325#[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 #[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 #[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 #[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 #[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
456pub struct RecoveryCompletion {
459 core: SessionCore,
460 outcome: RecoveryOutcome,
461}
462
463impl RecoveryCompletion {
464 #[must_use]
466 pub const fn outcome(&self) -> &RecoveryOutcome {
467 &self.outcome
468 }
469
470 pub fn affected_applications(&mut self) -> Result<AffectedApplications> {
472 self.core.affected_applications()
473 }
474
475 pub fn end(self) -> Result<RecoveryOutcome> {
479 let outcome = self.outcome;
480 self.core.end()?;
481 Ok(outcome)
482 }
483}
484
485pub 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 #[must_use]
498 pub const fn error(&self) -> &Error {
499 &self.error
500 }
501
502 #[must_use]
504 pub const fn state(&self) -> &T {
505 &self.state
506 }
507
508 #[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
537pub struct JoinedSession {
542 core: SessionCore,
543}
544
545impl JoinedSession {
546 pub fn join(key: &SessionKey) -> Result<Self> {
548 SessionCore::join(key).map(|core| Self { core })
549 }
550
551 #[must_use]
553 pub fn session_key(&self) -> &SessionKey {
554 &self.core.key
555 }
556
557 pub fn register_resources(&mut self, resources: &ResourceBatch) -> Result<()> {
559 self.core.register_resources(resources)
560 }
561
562 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 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 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 pub fn end(self) -> Result<()> {
603 self.core.end()
604 }
605}
606
607#[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 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 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 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(¬_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}