From 51e297bb8d346badc9b81c080207cc681642ef73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:35:39 +0000 Subject: [PATCH] Add a Services command that restarts the selected service Restart runs on the existing SCM worker. It stops the service, waits once within the published status hint, and starts only after the service is stopped. A failed stop or a failed start is reported and is not treated as success. Co-authored-by: jameslincn --- build_support/localization.rs | 4 + localization/de.toml | 3 + localization/en_us.toml | 3 + localization/es.toml | 3 + localization/fr.toml | 3 + localization/pt.toml | 3 + localization/ru.toml | 3 + localization/zh_cn.toml | 3 + localization/zh_tw.toml | 3 + src/app/mod.rs | 2 + src/pages/services/mod.rs | 42 +++++++++- src/pages/services/model.rs | 134 ++++++++++++++++++++++++++++++-- src/pages/services/sampler.rs | 117 ++++++++++++++++++++++------ src/ui/localization/text_key.rs | 2 + src/ui/menus.rs | 1 + src/ui/resource_ids.rs | 1 + 16 files changed, 292 insertions(+), 35 deletions(-) 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;