diff --git a/build_support/localization.rs b/build_support/localization.rs index 6e3e62f..679c4ab 100644 --- a/build_support/localization.rs +++ b/build_support/localization.rs @@ -118,6 +118,10 @@ const MENU_STATUS_HELP_KEYS: &[(&str, &str)] = &[ "IDM_SERVICE_CONTINUE", "crate::ui::resource_ids::IDM_SERVICE_CONTINUE", ), + ( + "IDM_SERVICE_RESTART", + "crate::ui::resource_ids::IDM_SERVICE_RESTART", + ), ( "IDM_SERVICE_GOTOPROCESS", "crate::ui::resource_ids::IDM_SERVICE_GOTOPROCESS", diff --git a/localization/de.toml b/localization/de.toml index 5bfd680..c2fe8cc 100644 --- a/localization/de.toml +++ b/localization/de.toml @@ -330,6 +330,7 @@ ServicesPageTitle = "Dienste" ServicesMenu = "&Dienste" ServiceStart = "&Starten" ServiceStop = "S&toppen" +ServiceRestart = "&Neu starten" ServicePause = "&Anhalten" ServiceContinue = "&Fortsetzen" ServiceColumnName = "Name" @@ -349,6 +350,7 @@ ServiceStartupManual = "Manuell" ServiceStartupDisabled = "Deaktiviert" ServiceStartFailed = "Der Dienst konnte nicht gestartet werden." ServiceStopFailed = "Der Dienst konnte nicht beendet werden." +ServiceRestartFailed = "Der Dienst konnte nicht neu gestartet werden." ServicePauseFailed = "Der Dienst konnte nicht angehalten werden." ServiceContinueFailed = "Der Dienst konnte nicht fortgesetzt werden." ServiceStateUnchanged = "Der Dienst hat den angeforderten Zustand nicht erreicht." @@ -417,6 +419,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "Startet den ausgewählten Dienst" IDM_SERVICE_STOP = "Beendet den ausgewählten Dienst" +IDM_SERVICE_RESTART = "Startet den ausgewählten Dienst neu" IDM_SERVICE_PAUSE = "Hält den ausgewählten Dienst an" IDM_SERVICE_CONTINUE = "Setzt den ausgewählten Dienst fort" IDM_SERVICE_GOTOPROCESS = "Wählt den Prozess des Diensts auf der Seite Prozesse aus" diff --git a/localization/en_us.toml b/localization/en_us.toml index 6f4802a..69d8f59 100644 --- a/localization/en_us.toml +++ b/localization/en_us.toml @@ -337,6 +337,7 @@ ServicesPageTitle = "Services" ServicesMenu = "&Services" ServiceStart = "&Start" ServiceStop = "&Stop" +ServiceRestart = "&Restart" ServicePause = "&Pause" ServiceContinue = "&Continue" ServiceColumnName = "Name" @@ -356,6 +357,7 @@ ServiceStartupManual = "Manual" ServiceStartupDisabled = "Disabled" ServiceStartFailed = "Unable to start the service." ServiceStopFailed = "Unable to stop the service." +ServiceRestartFailed = "Unable to restart the service." ServicePauseFailed = "Unable to pause the service." ServiceContinueFailed = "Unable to continue the service." ServiceStateUnchanged = "The service did not enter the requested state." @@ -424,6 +426,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "Starts the selected service" IDM_SERVICE_STOP = "Stops the selected service" +IDM_SERVICE_RESTART = "Restarts the selected service" IDM_SERVICE_PAUSE = "Pauses the selected service" IDM_SERVICE_CONTINUE = "Continues the selected service" IDM_SERVICE_GOTOPROCESS = "Selects the service process on the Processes page" diff --git a/localization/es.toml b/localization/es.toml index 35e19ea..e0fe1f5 100644 --- a/localization/es.toml +++ b/localization/es.toml @@ -330,6 +330,7 @@ ServicesPageTitle = "Servicios" ServicesMenu = "&Servicios" ServiceStart = "&Iniciar" ServiceStop = "&Detener" +ServiceRestart = "&Reiniciar" ServicePause = "&Pausar" ServiceContinue = "&Continuar" ServiceColumnName = "Nombre" @@ -349,6 +350,7 @@ ServiceStartupManual = "Manual" ServiceStartupDisabled = "Deshabilitado" ServiceStartFailed = "No se pudo iniciar el servicio." ServiceStopFailed = "No se pudo detener el servicio." +ServiceRestartFailed = "No se pudo reiniciar el servicio." ServicePauseFailed = "No se pudo pausar el servicio." ServiceContinueFailed = "No se pudo continuar el servicio." ServiceStateUnchanged = "El servicio no entró en el estado solicitado." @@ -417,6 +419,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "Inicia el servicio seleccionado" IDM_SERVICE_STOP = "Detiene el servicio seleccionado" +IDM_SERVICE_RESTART = "Reinicia el servicio seleccionado" IDM_SERVICE_PAUSE = "Pausa el servicio seleccionado" IDM_SERVICE_CONTINUE = "Continúa el servicio seleccionado" IDM_SERVICE_GOTOPROCESS = "Selecciona el proceso del servicio en la página Procesos" diff --git a/localization/fr.toml b/localization/fr.toml index c9b8992..b646759 100644 --- a/localization/fr.toml +++ b/localization/fr.toml @@ -330,6 +330,7 @@ ServicesPageTitle = "Services" ServicesMenu = "&Services" ServiceStart = "&Démarrer" ServiceStop = "&Arrêter" +ServiceRestart = "&Redémarrer" ServicePause = "&Suspendre" ServiceContinue = "&Continuer" ServiceColumnName = "Nom" @@ -349,6 +350,7 @@ ServiceStartupManual = "Manuel" ServiceStartupDisabled = "Désactivé" ServiceStartFailed = "Impossible de démarrer le service." ServiceStopFailed = "Impossible d'arrêter le service." +ServiceRestartFailed = "Impossible de redémarrer le service." ServicePauseFailed = "Impossible de suspendre le service." ServiceContinueFailed = "Impossible de continuer le service." ServiceStateUnchanged = "Le service n'a pas atteint l'état demandé." @@ -417,6 +419,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "Démarre le service sélectionné" IDM_SERVICE_STOP = "Arrête le service sélectionné" +IDM_SERVICE_RESTART = "Redémarre le service sélectionné" IDM_SERVICE_PAUSE = "Suspend le service sélectionné" IDM_SERVICE_CONTINUE = "Reprend le service sélectionné" IDM_SERVICE_GOTOPROCESS = "Sélectionne le processus du service dans la page Processus" diff --git a/localization/pt.toml b/localization/pt.toml index 66b2d54..79d9e47 100644 --- a/localization/pt.toml +++ b/localization/pt.toml @@ -330,6 +330,7 @@ ServicesPageTitle = "Serviços" ServicesMenu = "&Serviços" ServiceStart = "&Iniciar" ServiceStop = "&Parar" +ServiceRestart = "&Reiniciar" ServicePause = "&Pausar" ServiceContinue = "&Continuar" ServiceColumnName = "Nome" @@ -349,6 +350,7 @@ ServiceStartupManual = "Manual" ServiceStartupDisabled = "Desativado" ServiceStartFailed = "Não foi possível iniciar o serviço." ServiceStopFailed = "Não foi possível parar o serviço." +ServiceRestartFailed = "Não foi possível reiniciar o serviço." ServicePauseFailed = "Não foi possível pausar o serviço." ServiceContinueFailed = "Não foi possível continuar o serviço." ServiceStateUnchanged = "O serviço não entrou no estado solicitado." @@ -417,6 +419,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "Inicia o serviço selecionado" IDM_SERVICE_STOP = "Para o serviço selecionado" +IDM_SERVICE_RESTART = "Reinicia o serviço selecionado" IDM_SERVICE_PAUSE = "Pausa o serviço selecionado" IDM_SERVICE_CONTINUE = "Continua o serviço selecionado" IDM_SERVICE_GOTOPROCESS = "Seleciona o processo do serviço na página Processos" diff --git a/localization/ru.toml b/localization/ru.toml index 9845df7..52f9a57 100644 --- a/localization/ru.toml +++ b/localization/ru.toml @@ -330,6 +330,7 @@ ServicesPageTitle = "Службы" ServicesMenu = "&Службы" ServiceStart = "&Запустить" ServiceStop = "&Остановить" +ServiceRestart = "Пере&запустить" ServicePause = "&Приостановить" ServiceContinue = "&Продолжить" ServiceColumnName = "Имя" @@ -349,6 +350,7 @@ ServiceStartupManual = "Вручную" ServiceStartupDisabled = "Отключена" ServiceStartFailed = "Не удалось запустить службу." ServiceStopFailed = "Не удалось остановить службу." +ServiceRestartFailed = "Не удалось перезапустить службу." ServicePauseFailed = "Не удалось приостановить службу." ServiceContinueFailed = "Не удалось возобновить службу." ServiceStateUnchanged = "Служба не перешла в запрошенное состояние." @@ -417,6 +419,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "Запускает выбранную службу" IDM_SERVICE_STOP = "Останавливает выбранную службу" +IDM_SERVICE_RESTART = "Перезапускает выбранную службу" IDM_SERVICE_PAUSE = "Приостанавливает выбранную службу" IDM_SERVICE_CONTINUE = "Возобновляет выбранную службу" IDM_SERVICE_GOTOPROCESS = "Выбирает процесс службы на странице процессов" diff --git a/localization/zh_cn.toml b/localization/zh_cn.toml index 72ff3ad..18492a7 100644 --- a/localization/zh_cn.toml +++ b/localization/zh_cn.toml @@ -327,6 +327,7 @@ ServicesPageTitle = "服务" ServicesMenu = "服务(&S)" ServiceStart = "启动(&S)" ServiceStop = "停止(&T)" +ServiceRestart = "重新启动(&R)" ServicePause = "暂停(&P)" ServiceContinue = "继续(&C)" ServiceColumnName = "名称" @@ -346,6 +347,7 @@ ServiceStartupManual = "手动" ServiceStartupDisabled = "禁用" ServiceStartFailed = "无法启动该服务。" ServiceStopFailed = "无法停止该服务。" +ServiceRestartFailed = "无法重新启动该服务。" ServicePauseFailed = "无法暂停该服务。" ServiceContinueFailed = "无法继续该服务。" ServiceStateUnchanged = "该服务没有进入请求的状态。" @@ -414,6 +416,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "启动所选服务" IDM_SERVICE_STOP = "停止所选服务" +IDM_SERVICE_RESTART = "重新启动所选服务" IDM_SERVICE_PAUSE = "暂停所选服务" IDM_SERVICE_CONTINUE = "继续所选服务" IDM_SERVICE_GOTOPROCESS = "在进程页选中该服务的进程" diff --git a/localization/zh_tw.toml b/localization/zh_tw.toml index c7dbd0a..1dbb14c 100644 --- a/localization/zh_tw.toml +++ b/localization/zh_tw.toml @@ -327,6 +327,7 @@ ServicesPageTitle = "服務" ServicesMenu = "服務(&S)" ServiceStart = "啟動(&S)" ServiceStop = "停止(&T)" +ServiceRestart = "重新啟動(&R)" ServicePause = "暫停(&P)" ServiceContinue = "繼續(&C)" ServiceColumnName = "名稱" @@ -346,6 +347,7 @@ ServiceStartupManual = "手動" ServiceStartupDisabled = "已停用" ServiceStartFailed = "無法啟動服務。" ServiceStopFailed = "無法停止服務。" +ServiceRestartFailed = "無法重新啟動服務。" ServicePauseFailed = "無法暫停服務。" ServiceContinueFailed = "無法繼續服務。" ServiceStateUnchanged = "服務沒有進入要求的狀態。" @@ -414,6 +416,7 @@ IDM_TASK_TILEVERT = "Tiles the windows vertically on the desktop" IDM_TASK_BRINGTOFRONT = "Brings the window front, but does not switch to it" IDM_SERVICE_START = "啟動選取的服務" IDM_SERVICE_STOP = "停止選取的服務" +IDM_SERVICE_RESTART = "重新啟動選取的服務" IDM_SERVICE_PAUSE = "暫停選取的服務" IDM_SERVICE_CONTINUE = "繼續選取的服務" IDM_SERVICE_GOTOPROCESS = "在處理程序頁選取該服務的處理程序" diff --git a/src/app/mod.rs b/src/app/mod.rs index 0e65c3f..d23243b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1180,6 +1180,7 @@ impl App { .unwrap_or_default(); self.enable_menu_item(menu, IDM_SERVICE_START, commands.start); self.enable_menu_item(menu, IDM_SERVICE_STOP, commands.stop); + self.enable_menu_item(menu, IDM_SERVICE_RESTART, commands.restart); self.enable_menu_item(menu, IDM_SERVICE_PAUSE, commands.pause); self.enable_menu_item(menu, IDM_SERVICE_CONTINUE, commands.continue_service); self.enable_menu_item(menu, IDM_SERVICE_GOTOPROCESS, commands.go_to_process); @@ -2306,6 +2307,7 @@ impl App { } IDM_SERVICE_START | IDM_SERVICE_STOP + | IDM_SERVICE_RESTART | IDM_SERVICE_PAUSE | IDM_SERVICE_CONTINUE | IDM_SERVICE_GOTOPROCESS => { diff --git a/src/pages/services/mod.rs b/src/pages/services/mod.rs index b2b2aa9..f6dc32c 100644 --- a/src/pages/services/mod.rs +++ b/src/pages/services/mod.rs @@ -12,7 +12,7 @@ mod model; mod sampler; -// 服务页列出本机 Win32 服务,并在后台 worker 上启动、停止、暂停或继续服务。 +// 服务页列出本机 Win32 服务,并在后台 worker 上启动、停止、重新启动、暂停或继续服务。 // UI 线程只提交请求、维护列表选择和绘制。刷新保留服务名对应的选择、焦点和滚动位置。 // “转到进程”只使用带创建时间的 ProcIdentity;没有校验过的进程时该命令保持禁用。 @@ -48,8 +48,8 @@ use crate::ui::localization::{TextKey, service_column_titles, text}; use crate::ui::menus::build_popup_menu; use crate::ui::resource_ids::{ IDC_SERVICELIST, IDM_SERVICE_CONTINUE, IDM_SERVICE_GOTOPROCESS, IDM_SERVICE_PAUSE, - IDM_SERVICE_START, IDM_SERVICE_STOP, IDR_SERVICE_CONTEXT, PWM_SERVICE_WORKER_COMPLETE, - WM_FINDPROC, + IDM_SERVICE_RESTART, IDM_SERVICE_START, IDM_SERVICE_STOP, IDR_SERVICE_CONTEXT, + PWM_SERVICE_WORKER_COMPLETE, WM_FINDPROC, }; pub(crate) use self::model::ServiceCommandState; @@ -172,6 +172,7 @@ impl ServicePageState { state.start = false; state.stop = false; } + state.restart = state.stop; state } @@ -185,6 +186,10 @@ impl ServicePageState { self.request_control(ServiceControlAction::Stop); true } + IDM_SERVICE_RESTART => { + self.request_control(ServiceControlAction::Restart); + true + } IDM_SERVICE_PAUSE => { self.request_control(ServiceControlAction::Pause); true @@ -268,6 +273,7 @@ impl ServicePageState { unsafe { enable_command(popup.as_raw(), IDM_SERVICE_START, state.start); enable_command(popup.as_raw(), IDM_SERVICE_STOP, state.stop); + enable_command(popup.as_raw(), IDM_SERVICE_RESTART, state.restart); enable_command(popup.as_raw(), IDM_SERVICE_PAUSE, state.pause); enable_command(popup.as_raw(), IDM_SERVICE_CONTINUE, state.continue_service); enable_command(popup.as_raw(), IDM_SERVICE_GOTOPROCESS, state.go_to_process); @@ -378,6 +384,7 @@ impl ServicePageState { let allowed = match action { ServiceControlAction::Start => state.start, ServiceControlAction::Stop => state.stop, + ServiceControlAction::Restart => state.restart, ServiceControlAction::Pause => state.pause, ServiceControlAction::Continue => state.continue_service, }; @@ -704,6 +711,7 @@ impl ServicePageState { let summary = match action { ServiceControlAction::Start => text(TextKey::ServiceStartFailed), ServiceControlAction::Stop => text(TextKey::ServiceStopFailed), + ServiceControlAction::Restart => text(TextKey::ServiceRestartFailed), ServiceControlAction::Pause => text(TextKey::ServicePauseFailed), ServiceControlAction::Continue => text(TextKey::ServiceContinueFailed), }; @@ -870,6 +878,34 @@ mod tests { assert_eq!(page.last_refresh_error, Some(ERROR_BROKEN_PIPE)); } + #[test] + fn restart_is_enabled_exactly_when_stop_is_and_a_pending_control_disables_it() { + let mut page = ServicePageState { + rows: vec![service_row( + self::sampler::ServiceRecord { + service_name: "test-service".to_string(), + description: String::new(), + status: ServiceRunState::Running, + startup: ServiceStartupType::Manual, + accepts_stop: true, + accepts_pause_continue: false, + process: None, + }, + "PID", + )], + selected_service: Some("test-service".to_string()), + ..ServicePageState::default() + }; + let enabled = page.command_state(); + assert!(enabled.stop); + assert!(enabled.restart); + page.pending_control = Some(ServiceControlAction::Restart); + let pending = page.command_state(); + assert!(!pending.stop); + assert!(!pending.restart); + assert!(!pending.start); + } + #[test] fn a_queued_service_control_cannot_be_overwritten_by_another_selection() { let (started_sender, started_receiver) = crossbeam_channel::bounded(1); diff --git a/src/pages/services/model.rs b/src/pages/services/model.rs index 484d786..6eca878 100644 --- a/src/pages/services/model.rs +++ b/src/pages/services/model.rs @@ -57,6 +57,7 @@ pub(crate) enum ServiceStartupType { pub(crate) enum ServiceControlAction { Start, Stop, + Restart, Pause, Continue, } @@ -74,6 +75,7 @@ pub(crate) enum ServiceRequest { pub(crate) struct ServiceCommandState { pub(crate) start: bool, pub(crate) stop: bool, + pub(crate) restart: bool, pub(crate) pause: bool, pub(crate) continue_service: bool, pub(crate) go_to_process: bool, @@ -84,6 +86,7 @@ impl ServiceCommandState { Self { start: false, stop: false, + restart: false, pause: false, continue_service: false, go_to_process: false, @@ -145,14 +148,16 @@ pub(crate) fn command_state(selected: Option) -> ServiceCom let Some(selected) = selected else { return ServiceCommandState::disabled(); }; + let stop = selected.accepts_stop + && matches!( + selected.status, + ServiceRunState::Running | ServiceRunState::Paused + ); ServiceCommandState { start: selected.status == ServiceRunState::Stopped && selected.startup != ServiceStartupType::Disabled, - stop: selected.accepts_stop - && matches!( - selected.status, - ServiceRunState::Running | ServiceRunState::Paused - ), + stop, + restart: stop, go_to_process: service_has_valid_process_id(selected.status) && selected.process.is_some_and(ProcIdentity::is_verified), pause: selected.accepts_pause_continue && selected.status == ServiceRunState::Running, @@ -184,6 +189,12 @@ pub(crate) fn control_reached_requested_state( state, ServiceRunState::Stopped | ServiceRunState::StopPending ), + // Restart's start half uses the same accepted states as Start. Stop-pending, + // stopped, and unknown are not a successful restart. + ServiceControlAction::Restart => matches!( + state, + ServiceRunState::Running | ServiceRunState::StartPending + ), ServiceControlAction::Pause => matches!( state, ServiceRunState::Paused | ServiceRunState::PausePending @@ -206,6 +217,23 @@ pub(crate) fn classify_control_result( } } +/// Stop has finished only when the service is stopped. Pending and unknown are not completion. +pub(crate) fn stop_has_completed(state: ServiceRunState) -> bool { + state == ServiceRunState::Stopped +} + +/// Starts a restart only after stop completed. A stop error or any other observed state is +/// returned unchanged and `start` is not called. +pub(crate) fn restart_after_stop( + stop: Result, + start: impl FnOnce() -> Result, +) -> Result { + match stop { + Ok(state) if stop_has_completed(state) => start(), + other => other, + } +} + /// Keeps a pending start or stop ahead of a coalesced refresh. pub(crate) fn merge_service_requests(pending: &mut ServiceRequest, incoming: ServiceRequest) { match (&*pending, &incoming) { @@ -307,6 +335,7 @@ mod tests { ))); assert!(stopped.start); assert!(!stopped.stop); + assert!(!stopped.restart); assert!(!stopped.pause); assert!(!stopped.continue_service); assert!(!stopped.go_to_process); @@ -325,6 +354,7 @@ mod tests { ))); assert!(!running.start); assert!(running.stop); + assert!(running.restart); assert!(running.pause); assert!(!running.continue_service); assert!(!running.go_to_process); @@ -335,6 +365,7 @@ mod tests { Some(ProcIdentity::new(44, 100)), ))); assert!(verified.stop); + assert!(verified.restart); assert!(!verified.pause); assert!(verified.continue_service); assert!(verified.go_to_process); @@ -346,6 +377,7 @@ mod tests { ))); assert!(!pending.start); assert!(!pending.stop); + assert!(!pending.restart); assert!(!pending.pause); assert!(!pending.continue_service); assert!(!pending.go_to_process); @@ -356,9 +388,13 @@ mod tests { for status in [ServiceRunState::Running, ServiceRunState::Paused] { let mut selected = input(status, ServiceStartupType::Automatic, None); selected.accepts_stop = false; - assert!(!command_state(Some(selected)).stop); + let blocked = command_state(Some(selected)); + assert!(!blocked.stop); + assert!(!blocked.restart); selected.accepts_stop = true; - assert!(command_state(Some(selected)).stop); + let allowed = command_state(Some(selected)); + assert!(allowed.stop); + assert!(allowed.restart); } } @@ -477,6 +513,90 @@ mod tests { ); } + #[test] + fn restart_starts_only_after_a_completed_stop_and_unknown_is_not_success() { + assert!(stop_has_completed(ServiceRunState::Stopped)); + assert!(!stop_has_completed(ServiceRunState::StopPending)); + assert!(!stop_has_completed(ServiceRunState::Running)); + assert!(!stop_has_completed(ServiceRunState::Unknown(0))); + + let mut starts = 0u32; + let rejected = restart_after_stop(Err(1060), || { + starts += 1; + Ok(ServiceRunState::Running) + }); + assert_eq!(rejected, Err(1060)); + assert_eq!(starts, 0); + + for state in [ + ServiceRunState::StopPending, + ServiceRunState::Running, + ServiceRunState::Paused, + ServiceRunState::Unknown(7), + ] { + let observed = restart_after_stop(Ok(state), || { + starts += 1; + Ok(ServiceRunState::Running) + }); + assert_eq!(observed, Ok(state)); + } + assert_eq!(starts, 0); + + let start_error = restart_after_stop(Ok(ServiceRunState::Stopped), || { + starts += 1; + Err(5) + }); + assert_eq!(start_error, Err(5)); + assert_eq!(starts, 1); + assert_eq!( + classify_control_result(ServiceControlAction::Restart, start_error), + Err(ServiceActionFailure::Api(5)) + ); + + assert!(control_reached_requested_state( + ServiceControlAction::Restart, + ServiceRunState::Running + )); + assert!(control_reached_requested_state( + ServiceControlAction::Restart, + ServiceRunState::StartPending + )); + assert!(!control_reached_requested_state( + ServiceControlAction::Restart, + ServiceRunState::Stopped + )); + assert!(!control_reached_requested_state( + ServiceControlAction::Restart, + ServiceRunState::StopPending + )); + assert!(!control_reached_requested_state( + ServiceControlAction::Restart, + ServiceRunState::Unknown(0) + )); + assert_eq!( + classify_control_result( + ServiceControlAction::Restart, + Ok(ServiceRunState::StartPending) + ), + Ok(ServiceRunState::StartPending) + ); + assert_eq!( + classify_control_result( + ServiceControlAction::Restart, + Ok(ServiceRunState::Unknown(9)) + ), + Err(ServiceActionFailure::StateUnchanged { + observed: ServiceRunState::Unknown(9) + }) + ); + assert_eq!( + classify_control_result(ServiceControlAction::Restart, Ok(ServiceRunState::Stopped)), + Err(ServiceActionFailure::StateUnchanged { + observed: ServiceRunState::Stopped + }) + ); + } + #[test] fn refresh_does_not_replace_a_pending_control() { let mut pending = ServiceRequest::Control { diff --git a/src/pages/services/sampler.rs b/src/pages/services/sampler.rs index c46479b..8c0055e 100644 --- a/src/pages/services/sampler.rs +++ b/src/pages/services/sampler.rs @@ -9,11 +9,15 @@ // 作者: OpenAI Codex // -------------------------------------------------------------------------- -//! Service Control Manager queries and start, stop, pause, and continue calls. +//! Service Control Manager queries and start, stop, restart, pause, and continue calls. //! //! The page worker owns every SCM handle. Enumeration asks only for connect and enumerate rights. //! Each service is opened with query rights, and a control opens that one service with only the -//! right the action needs plus status query so the result can be checked. +//! rights that action needs plus status query so the result can be checked. Restart opens with +//! stop and start on that same handle. It sends stop, then starts only after stop has completed. +//! That completion wait is a single delay bounded by the wait hint on the status query Start and +//! Stop already use, and it is capped so a huge hint cannot run without limit. It does not retry. +//! Pending and unknown states are not completion. use std::collections::HashMap; use std::mem::{size_of, zeroed}; @@ -33,7 +37,7 @@ use windows_sys::Win32::System::Services::{ StartServiceW, }; use windows_sys::Win32::System::Threading::{ - PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW, + PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW, Sleep, }; use crate::infrastructure::native::to_wide_null; @@ -43,12 +47,14 @@ use crate::system::process_identity::{ use super::model::{ OwningProcess, ServiceActionFailure, ServiceControlAction, ServiceRequest, ServiceRunState, - ServiceStartupType, classify_control_result, file_name_from_win32_path, - service_has_valid_process_id, startup_from_scm, state_from_scm, + ServiceStartupType, classify_control_result, file_name_from_win32_path, restart_after_stop, + service_has_valid_process_id, startup_from_scm, state_from_scm, stop_has_completed, }; const ENUM_ATTEMPT_LIMIT: usize = 10_000; const CONFIG_GROW_LIMIT: usize = 8; +/// Cap for the one stop-completion wait. The service's `dwWaitHint` is the requested bound. +const SERVICE_STOP_WAIT_CAP_MS: u32 = 30_000; #[derive(Clone, Debug, PartialEq, Eq)] pub(super) struct ServiceRecord { @@ -491,39 +497,100 @@ fn control_service_and_read_state( let access = match action { ServiceControlAction::Start => SERVICE_START | SERVICE_QUERY_STATUS, ServiceControlAction::Stop => SERVICE_STOP | SERVICE_QUERY_STATUS, + ServiceControlAction::Restart => SERVICE_STOP | SERVICE_START | SERVICE_QUERY_STATUS, ServiceControlAction::Pause | ServiceControlAction::Continue => { SERVICE_PAUSE_CONTINUE | SERVICE_QUERY_STATUS } }; let service = open_service(manager.as_raw(), service_name, access)?; - let control = match action { + match action { ServiceControlAction::Start => { - // SAFETY: the service is open for SERVICE_START. No start arguments are supplied. - let success = unsafe { StartServiceW(service.as_raw(), 0, std::ptr::null()) }; - if success == 0 { - return Err(last_error()); - } - None + start_open_service(service.as_raw())?; + query_service_state(service.as_raw()) } - ServiceControlAction::Stop => Some(SERVICE_CONTROL_STOP), - ServiceControlAction::Pause => Some(SERVICE_CONTROL_PAUSE), - ServiceControlAction::Continue => Some(SERVICE_CONTROL_CONTINUE), - }; - if let Some(control) = control { - let mut status = unsafe { zeroed::() }; - // SAFETY: the service is open for this one control right. `status` is a writable output - // of the control call and is not used as proof of the final state. - let success = unsafe { ControlService(service.as_raw(), control, &mut status) }; - if success == 0 { - return Err(last_error()); + ServiceControlAction::Stop => { + send_service_control(service.as_raw(), SERVICE_CONTROL_STOP)?; + query_service_state(service.as_raw()) + } + ServiceControlAction::Pause => { + send_service_control(service.as_raw(), SERVICE_CONTROL_PAUSE)?; + query_service_state(service.as_raw()) } + ServiceControlAction::Continue => { + send_service_control(service.as_raw(), SERVICE_CONTROL_CONTINUE)?; + query_service_state(service.as_raw()) + } + ServiceControlAction::Restart => restart_open_service(service.as_raw()), + } +} + +fn restart_open_service( + service: windows_sys::Win32::System::Services::SC_HANDLE, +) -> Result { + // The stop control returns when the handler accepts it. Completion is then bounded by the + // wait hint on that status. Only SERVICE_STOPPED continues into start. A missing service, + // a rejected control, or a status query failure stays that error and does not start. + let stop = send_service_control(service, SERVICE_CONTROL_STOP) + .and_then(|_| wait_until_service_stopped(service)); + restart_after_stop(stop, || { + start_open_service(service)?; + query_service_state(service) + }) +} + +fn wait_until_service_stopped( + service: windows_sys::Win32::System::Services::SC_HANDLE, +) -> Result { + let status = query_service_status(service)?; + let state = state_from_scm(status.dwCurrentState); + if stop_has_completed(state) || !matches!(state, ServiceRunState::StopPending) { + return Ok(state); + } + let bound_ms = status.dwWaitHint.min(SERVICE_STOP_WAIT_CAP_MS); + if bound_ms > 0 { + // SAFETY: Sleep delays only this SCM worker. One wait, bounded by the status hint and + // SERVICE_STOP_WAIT_CAP_MS. This is not a retry loop. + unsafe { Sleep(bound_ms) }; + } + query_service_state(service) +} + +fn start_open_service(service: windows_sys::Win32::System::Services::SC_HANDLE) -> Result<(), u32> { + // SAFETY: the service is open for SERVICE_START. No start arguments are supplied. + let success = unsafe { StartServiceW(service, 0, std::ptr::null()) }; + if success == 0 { + Err(last_error()) + } else { + Ok(()) + } +} + +fn send_service_control( + service: windows_sys::Win32::System::Services::SC_HANDLE, + control: u32, +) -> Result<(), u32> { + let mut status = unsafe { zeroed::() }; + // SAFETY: the service is open for this one control right. `status` is a writable output of + // the control call and is not used as proof of the final state. + let success = unsafe { ControlService(service, control, &mut status) }; + if success == 0 { + Err(last_error()) + } else { + Ok(()) } - query_service_state(service.as_raw()) } fn query_service_state( service: windows_sys::Win32::System::Services::SC_HANDLE, ) -> Result { + Ok(state_from_scm( + query_service_status(service)?.dwCurrentState, + )) +} + +fn query_service_status( + service: windows_sys::Win32::System::Services::SC_HANDLE, +) -> Result { let mut status = unsafe { zeroed::() }; let mut bytes_needed = 0u32; // SAFETY: the service is open for SERVICE_QUERY_STATUS. The status object is writable and @@ -540,7 +607,7 @@ fn query_service_state( if success == 0 { return Err(last_error()); } - Ok(state_from_scm(status.dwCurrentState)) + Ok(status) } fn open_service( diff --git a/src/ui/localization/text_key.rs b/src/ui/localization/text_key.rs index 1b0fc7d..8fa7147 100644 --- a/src/ui/localization/text_key.rs +++ b/src/ui/localization/text_key.rs @@ -338,6 +338,7 @@ pub enum TextKey { ServicesMenu, ServiceStart, ServiceStop, + ServiceRestart, ServicePause, ServiceContinue, ServiceColumnName, @@ -357,6 +358,7 @@ pub enum TextKey { ServiceStartupDisabled, ServiceStartFailed, ServiceStopFailed, + ServiceRestartFailed, ServicePauseFailed, ServiceContinueFailed, ServiceStateUnchanged, diff --git a/src/ui/menus.rs b/src/ui/menus.rs index 541ae10..650eb64 100644 --- a/src/ui/menus.rs +++ b/src/ui/menus.rs @@ -324,6 +324,7 @@ fn build_service_command_menu() -> MenuResult { let mut menu = PopupMenu::new()?; append_item(&mut menu, IDM_SERVICE_START, TextKey::ServiceStart)?; append_item(&mut menu, IDM_SERVICE_STOP, TextKey::ServiceStop)?; + append_item(&mut menu, IDM_SERVICE_RESTART, TextKey::ServiceRestart)?; append_item(&mut menu, IDM_SERVICE_PAUSE, TextKey::ServicePause)?; append_item(&mut menu, IDM_SERVICE_CONTINUE, TextKey::ServiceContinue)?; append_separator(&mut menu)?; diff --git a/src/ui/resource_ids.rs b/src/ui/resource_ids.rs index e4eb2db..9489400 100644 --- a/src/ui/resource_ids.rs +++ b/src/ui/resource_ids.rs @@ -396,6 +396,7 @@ pub const IDM_SERVICE_STOP: u16 = 40121; pub const IDM_SERVICE_GOTOPROCESS: u16 = 40122; pub const IDM_SERVICE_PAUSE: u16 = 40123; pub const IDM_SERVICE_CONTINUE: u16 = 40124; +pub const IDM_SERVICE_RESTART: u16 = 40125; pub const IDM_PROC_TREE: u16 = 40130; pub const IDM_PROC_EXPAND: u16 = 40131; pub const IDM_PROC_COLLAPSE: u16 = 40132;