From 78fde0a06c82460aedc130397cd7a1c3e474c4a6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:15:47 +0000 Subject: [PATCH] feat: pause and continue the selected service Services can pause a running service and continue a paused one through the existing background worker, using the same failure dialog as start and stop. Co-authored-by: jameslincn --- README.md | 2 +- README.zh-CN.md | 2 +- build_support/localization.rs | 8 +++++ localization/de.toml | 6 ++++ localization/en_us.toml | 6 ++++ localization/es.toml | 6 ++++ localization/fr.toml | 6 ++++ localization/pt.toml | 6 ++++ localization/ru.toml | 6 ++++ localization/zh_cn.toml | 6 ++++ localization/zh_tw.toml | 6 ++++ src/app/mod.rs | 8 ++++- src/pages/services/mod.rs | 25 ++++++++++++-- src/pages/services/model.rs | 60 +++++++++++++++++++++++++++++++++ src/pages/services/sampler.rs | 35 +++++++++++-------- src/ui/dialogs.rs | 14 ++++++-- src/ui/localization/text_key.rs | 4 +++ src/ui/localization/ui.rs | 6 ++++ src/ui/menus.rs | 2 ++ src/ui/resource_ids.rs | 2 ++ 20 files changed, 194 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 089db7a..ed075c8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This is not a copy of the modern Task Manager. CPU and GPU details come from sys - **CPU** shows the processor name, effective frequency, topology, caches, virtualization, ISA features, system rates, and a 60-second utilization graph. - **GPU** handles multiple adapters and engines, dedicated and shared memory, temperature, driver details, and DirectX information. - **Networking and Users** show adapter throughput history and Windows session state. -- **Services** lists Win32 services with description, status, startup type, and the owning process. Start, stop, and Go to Process run only when the current row allows them. Go to Process requires a process id plus creation time. +- **Services** lists Win32 services with description, status, startup type, and the owning process. Start, stop, pause, continue, and Go to Process run only when the current row allows them. Go to Process requires a process id plus creation time. ## Download diff --git a/README.zh-CN.md b/README.zh-CN.md index ab51ce2..f7f357c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -16,7 +16,7 @@ - **CPU**:处理器型号、有效频率、拓扑、缓存、虚拟化、ISA、系统速率计数和 60 秒总利用率历史。 - **GPU**:多适配器、多引擎、专用/共享显存、温度、驱动和 DirectX 信息。 - **网络与用户**:网卡吞吐历史、会话状态和常用会话操作。 -- **服务**:列出 Win32 服务的名称、描述、状态、启动类型和所属进程。启动、停止和转到进程只在当前行允许时可用。转到进程同时要求进程 ID 和创建时间。 +- **服务**:列出 Win32 服务的名称、描述、状态、启动类型和所属进程。启动、停止、暂停、继续和转到进程只在当前行允许时可用。转到进程同时要求进程 ID 和创建时间。 ## 下载 diff --git a/build_support/localization.rs b/build_support/localization.rs index 53d3068..87fc1f2 100644 --- a/build_support/localization.rs +++ b/build_support/localization.rs @@ -110,6 +110,14 @@ const MENU_STATUS_HELP_KEYS: &[(&str, &str)] = &[ "IDM_SERVICE_STOP", "crate::ui::resource_ids::IDM_SERVICE_STOP", ), + ( + "IDM_SERVICE_PAUSE", + "crate::ui::resource_ids::IDM_SERVICE_PAUSE", + ), + ( + "IDM_SERVICE_CONTINUE", + "crate::ui::resource_ids::IDM_SERVICE_CONTINUE", + ), ( "IDM_SERVICE_GOTOPROCESS", "crate::ui::resource_ids::IDM_SERVICE_GOTOPROCESS", diff --git a/localization/de.toml b/localization/de.toml index 0c47338..b6ffdec 100644 --- a/localization/de.toml +++ b/localization/de.toml @@ -308,6 +308,8 @@ ServicesPageTitle = "Dienste" ServicesMenu = "&Dienste" ServiceStart = "&Starten" ServiceStop = "S&toppen" +ServicePause = "&Anhalten" +ServiceContinue = "&Fortsetzen" ServiceColumnName = "Name" ServiceColumnDescription = "Beschreibung" ServiceColumnStartupType = "Starttyp" @@ -325,6 +327,8 @@ ServiceStartupManual = "Manuell" ServiceStartupDisabled = "Deaktiviert" ServiceStartFailed = "Der Dienst konnte nicht gestartet werden." ServiceStopFailed = "Der Dienst konnte nicht beendet werden." +ServicePauseFailed = "Der Dienst konnte nicht angehalten werden." +ServiceContinueFailed = "Der Dienst konnte nicht fortgesetzt werden." ServiceStateUnchanged = "Der Dienst hat den angeforderten Zustand nicht erreicht." ServiceProcessPid = "PID" DisconnectStatusHelp = "Trennt die ausgewaehlte Benutzersitzung" @@ -364,6 +368,8 @@ 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_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" IDM_PROC_TREE = "Gruppiert Prozesse unter dem übergeordneten Prozess, der sie erstellt hat" IDM_PROC_EXPAND = "Zeigt die untergeordneten Prozesse der Auswahl" diff --git a/localization/en_us.toml b/localization/en_us.toml index 220bc4a..ae36cca 100644 --- a/localization/en_us.toml +++ b/localization/en_us.toml @@ -315,6 +315,8 @@ ServicesPageTitle = "Services" ServicesMenu = "&Services" ServiceStart = "&Start" ServiceStop = "&Stop" +ServicePause = "&Pause" +ServiceContinue = "&Continue" ServiceColumnName = "Name" ServiceColumnDescription = "Description" ServiceColumnStartupType = "Startup Type" @@ -332,6 +334,8 @@ ServiceStartupManual = "Manual" ServiceStartupDisabled = "Disabled" ServiceStartFailed = "Unable to start the service." ServiceStopFailed = "Unable to stop the service." +ServicePauseFailed = "Unable to pause the service." +ServiceContinueFailed = "Unable to continue the service." ServiceStateUnchanged = "The service did not enter the requested state." ServiceProcessPid = "PID" DisconnectStatusHelp = "Disconnects the selected user session" @@ -371,6 +375,8 @@ 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_PAUSE = "Pauses the selected service" +IDM_SERVICE_CONTINUE = "Continues the selected service" IDM_SERVICE_GOTOPROCESS = "Selects the service process on the Processes page" IDM_PROC_TREE = "Groups processes under the parent that created them" IDM_PROC_EXPAND = "Shows the child processes of the selection" diff --git a/localization/es.toml b/localization/es.toml index 5c418f8..5ad9903 100644 --- a/localization/es.toml +++ b/localization/es.toml @@ -308,6 +308,8 @@ ServicesPageTitle = "Servicios" ServicesMenu = "&Servicios" ServiceStart = "&Iniciar" ServiceStop = "&Detener" +ServicePause = "&Pausar" +ServiceContinue = "&Continuar" ServiceColumnName = "Nombre" ServiceColumnDescription = "Descripción" ServiceColumnStartupType = "Tipo de inicio" @@ -325,6 +327,8 @@ ServiceStartupManual = "Manual" ServiceStartupDisabled = "Deshabilitado" ServiceStartFailed = "No se pudo iniciar el servicio." ServiceStopFailed = "No se pudo detener 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." ServiceProcessPid = "PID" DisconnectStatusHelp = "Desconecta la sesión del usuario seleccionado" @@ -364,6 +368,8 @@ 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_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" IDM_PROC_TREE = "Agrupa los procesos bajo el proceso primario que los creó" IDM_PROC_EXPAND = "Muestra los procesos secundarios de la selección" diff --git a/localization/fr.toml b/localization/fr.toml index b2e3a26..cf59b59 100644 --- a/localization/fr.toml +++ b/localization/fr.toml @@ -308,6 +308,8 @@ ServicesPageTitle = "Services" ServicesMenu = "&Services" ServiceStart = "&Démarrer" ServiceStop = "&Arrêter" +ServicePause = "&Suspendre" +ServiceContinue = "&Continuer" ServiceColumnName = "Nom" ServiceColumnDescription = "Description" ServiceColumnStartupType = "Type de démarrage" @@ -325,6 +327,8 @@ ServiceStartupManual = "Manuel" ServiceStartupDisabled = "Désactivé" ServiceStartFailed = "Impossible de démarrer le service." ServiceStopFailed = "Impossible d'arrêter le service." +ServicePauseFailed = "Impossible de suspendre le service." +ServiceContinueFailed = "Impossible de continuer le service." ServiceStateUnchanged = "Le service n'a pas atteint l'état demandé." ServiceProcessPid = "PID" DisconnectStatusHelp = "Déconnecte la session de l'utilisateur sélectionné" @@ -364,6 +368,8 @@ 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_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" IDM_PROC_TREE = "Regroupe les processus sous le parent qui les a créés" IDM_PROC_EXPAND = "Affiche les processus enfants de la sélection" diff --git a/localization/pt.toml b/localization/pt.toml index 4129216..59d432f 100644 --- a/localization/pt.toml +++ b/localization/pt.toml @@ -308,6 +308,8 @@ ServicesPageTitle = "Serviços" ServicesMenu = "&Serviços" ServiceStart = "&Iniciar" ServiceStop = "&Parar" +ServicePause = "&Pausar" +ServiceContinue = "&Continuar" ServiceColumnName = "Nome" ServiceColumnDescription = "Descrição" ServiceColumnStartupType = "Tipo de inicialização" @@ -325,6 +327,8 @@ ServiceStartupManual = "Manual" ServiceStartupDisabled = "Desativado" ServiceStartFailed = "Não foi possível iniciar o serviço." ServiceStopFailed = "Não foi possível parar 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." ServiceProcessPid = "PID" DisconnectStatusHelp = "Desconecta a sessão do usuário selecionado" @@ -364,6 +368,8 @@ 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_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" IDM_PROC_TREE = "Agrupa os processos sob o processo pai que os criou" IDM_PROC_EXPAND = "Mostra os processos filhos da seleção" diff --git a/localization/ru.toml b/localization/ru.toml index 28b0c9e..49c29b7 100644 --- a/localization/ru.toml +++ b/localization/ru.toml @@ -308,6 +308,8 @@ ServicesPageTitle = "Службы" ServicesMenu = "&Службы" ServiceStart = "&Запустить" ServiceStop = "&Остановить" +ServicePause = "&Приостановить" +ServiceContinue = "&Продолжить" ServiceColumnName = "Имя" ServiceColumnDescription = "Описание" ServiceColumnStartupType = "Тип запуска" @@ -325,6 +327,8 @@ ServiceStartupManual = "Вручную" ServiceStartupDisabled = "Отключена" ServiceStartFailed = "Не удалось запустить службу." ServiceStopFailed = "Не удалось остановить службу." +ServicePauseFailed = "Не удалось приостановить службу." +ServiceContinueFailed = "Не удалось возобновить службу." ServiceStateUnchanged = "Служба не перешла в запрошенное состояние." ServiceProcessPid = "PID" DisconnectStatusHelp = "Отключает выбранный сеанс пользователя" @@ -364,6 +368,8 @@ 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_PAUSE = "Приостанавливает выбранную службу" +IDM_SERVICE_CONTINUE = "Возобновляет выбранную службу" IDM_SERVICE_GOTOPROCESS = "Выбирает процесс службы на странице процессов" IDM_PROC_TREE = "Группирует процессы под родительским процессом, который их создал" IDM_PROC_EXPAND = "Показывает дочерние процессы выбранного элемента" diff --git a/localization/zh_cn.toml b/localization/zh_cn.toml index 3af90f1..cc77934 100644 --- a/localization/zh_cn.toml +++ b/localization/zh_cn.toml @@ -305,6 +305,8 @@ ServicesPageTitle = "服务" ServicesMenu = "服务(&S)" ServiceStart = "启动(&S)" ServiceStop = "停止(&T)" +ServicePause = "暂停(&P)" +ServiceContinue = "继续(&C)" ServiceColumnName = "名称" ServiceColumnDescription = "描述" ServiceColumnStartupType = "启动类型" @@ -322,6 +324,8 @@ ServiceStartupManual = "手动" ServiceStartupDisabled = "禁用" ServiceStartFailed = "无法启动该服务。" ServiceStopFailed = "无法停止该服务。" +ServicePauseFailed = "无法暂停该服务。" +ServiceContinueFailed = "无法继续该服务。" ServiceStateUnchanged = "该服务没有进入请求的状态。" ServiceProcessPid = "PID" DisconnectStatusHelp = "断开所选用户会话" @@ -361,6 +365,8 @@ 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_PAUSE = "暂停所选服务" +IDM_SERVICE_CONTINUE = "继续所选服务" IDM_SERVICE_GOTOPROCESS = "在进程页选中该服务的进程" IDM_PROC_TREE = "按创建该进程的父进程分组显示" IDM_PROC_EXPAND = "显示所选进程的子进程" diff --git a/localization/zh_tw.toml b/localization/zh_tw.toml index 9ca48d8..5b9c5ac 100644 --- a/localization/zh_tw.toml +++ b/localization/zh_tw.toml @@ -305,6 +305,8 @@ ServicesPageTitle = "服務" ServicesMenu = "服務(&S)" ServiceStart = "啟動(&S)" ServiceStop = "停止(&T)" +ServicePause = "暫停(&P)" +ServiceContinue = "繼續(&C)" ServiceColumnName = "名稱" ServiceColumnDescription = "描述" ServiceColumnStartupType = "啟動類型" @@ -322,6 +324,8 @@ ServiceStartupManual = "手動" ServiceStartupDisabled = "已停用" ServiceStartFailed = "無法啟動服務。" ServiceStopFailed = "無法停止服務。" +ServicePauseFailed = "無法暫停服務。" +ServiceContinueFailed = "無法繼續服務。" ServiceStateUnchanged = "服務沒有進入要求的狀態。" ServiceProcessPid = "PID" DisconnectStatusHelp = "中斷選取的使用者工作階段" @@ -361,6 +365,8 @@ 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_PAUSE = "暫停選取的服務" +IDM_SERVICE_CONTINUE = "繼續選取的服務" IDM_SERVICE_GOTOPROCESS = "在處理程序頁選取該服務的處理程序" IDM_PROC_TREE = "依建立該處理程序的父處理程序分組顯示" IDM_PROC_EXPAND = "顯示選取處理程序的子處理程序" diff --git a/src/app/mod.rs b/src/app/mod.rs index 8d5d1e8..80d21a3 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1178,6 +1178,8 @@ 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_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); } if self.options.current_page == PageId::Processes.persisted() { @@ -2288,7 +2290,11 @@ impl App { MessageBeep(0); } } - IDM_SERVICE_START | IDM_SERVICE_STOP | IDM_SERVICE_GOTOPROCESS => { + IDM_SERVICE_START + | IDM_SERVICE_STOP + | IDM_SERVICE_PAUSE + | IDM_SERVICE_CONTINUE + | IDM_SERVICE_GOTOPROCESS => { if self.options.current_page == PageId::Services.persisted() { let handled = self.pages[PageId::Services.index()].handle_service_command(command_id); diff --git a/src/pages/services/mod.rs b/src/pages/services/mod.rs index b38d1c3..b4d90f6 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;没有校验过的进程时该命令保持禁用。 @@ -47,8 +47,9 @@ use crate::system::process_identity::{FindProcPayload, ProcIdentity, find_proc_l 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_GOTOPROCESS, IDM_SERVICE_START, IDM_SERVICE_STOP, - IDR_SERVICE_CONTEXT, PWM_SERVICE_WORKER_COMPLETE, WM_FINDPROC, + 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, }; pub(crate) use self::model::ServiceCommandState; @@ -174,6 +175,14 @@ impl ServicePageState { self.request_control(ServiceControlAction::Stop); true } + IDM_SERVICE_PAUSE => { + self.request_control(ServiceControlAction::Pause); + true + } + IDM_SERVICE_CONTINUE => { + self.request_control(ServiceControlAction::Continue); + true + } IDM_SERVICE_GOTOPROCESS => { self.go_to_process(); true @@ -249,6 +258,8 @@ 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_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); let command = TrackPopupMenuEx( popup.as_raw(), @@ -301,6 +312,8 @@ impl ServicePageState { for control_id in [ i32::from(IDM_SERVICE_START), i32::from(IDM_SERVICE_STOP), + i32::from(IDM_SERVICE_PAUSE), + i32::from(IDM_SERVICE_CONTINUE), i32::from(IDM_SERVICE_GOTOPROCESS), ] { let control = GetDlgItem(self.hwnd, control_id); @@ -350,6 +363,8 @@ impl ServicePageState { let allowed = match action { ServiceControlAction::Start => state.start, ServiceControlAction::Stop => state.stop, + ServiceControlAction::Pause => state.pause, + ServiceControlAction::Continue => state.continue_service, }; let Some(service_name) = self.selected_service.clone() else { self.beep(); @@ -603,6 +618,8 @@ impl ServicePageState { let state = self.command_state(); enable_button(self.hwnd, IDM_SERVICE_START, state.start); enable_button(self.hwnd, IDM_SERVICE_STOP, state.stop); + enable_button(self.hwnd, IDM_SERVICE_PAUSE, state.pause); + enable_button(self.hwnd, IDM_SERVICE_CONTINUE, state.continue_service); enable_button(self.hwnd, IDM_SERVICE_GOTOPROCESS, state.go_to_process); } @@ -639,6 +656,8 @@ impl ServicePageState { let summary = match action { ServiceControlAction::Start => text(TextKey::ServiceStartFailed), ServiceControlAction::Stop => text(TextKey::ServiceStopFailed), + ServiceControlAction::Pause => text(TextKey::ServicePauseFailed), + ServiceControlAction::Continue => text(TextKey::ServiceContinueFailed), }; let body = match failure { ServiceActionFailure::Api(code) => { diff --git a/src/pages/services/model.rs b/src/pages/services/model.rs index 205c575..01a5507 100644 --- a/src/pages/services/model.rs +++ b/src/pages/services/model.rs @@ -57,6 +57,8 @@ pub(crate) enum ServiceStartupType { pub(crate) enum ServiceControlAction { Start, Stop, + Pause, + Continue, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -72,6 +74,8 @@ pub(crate) enum ServiceRequest { pub(crate) struct ServiceCommandState { pub(crate) start: bool, pub(crate) stop: bool, + pub(crate) pause: bool, + pub(crate) continue_service: bool, pub(crate) go_to_process: bool, } @@ -80,6 +84,8 @@ impl ServiceCommandState { Self { start: false, stop: false, + pause: false, + continue_service: false, go_to_process: false, } } @@ -144,6 +150,8 @@ pub(crate) fn command_state(selected: Option) -> ServiceCom selected.status, ServiceRunState::Running | ServiceRunState::Paused ), + pause: selected.status == ServiceRunState::Running, + continue_service: selected.status == ServiceRunState::Paused, go_to_process: selected.process.is_some_and(ProcIdentity::is_verified), } } @@ -161,6 +169,14 @@ pub(crate) fn control_reached_requested_state( state, ServiceRunState::Stopped | ServiceRunState::StopPending ), + ServiceControlAction::Pause => matches!( + state, + ServiceRunState::Paused | ServiceRunState::PausePending + ), + ServiceControlAction::Continue => matches!( + state, + ServiceRunState::Running | ServiceRunState::ContinuePending + ), } } @@ -274,6 +290,8 @@ mod tests { ))); assert!(stopped.start); assert!(!stopped.stop); + assert!(!stopped.pause); + assert!(!stopped.continue_service); assert!(!stopped.go_to_process); let disabled = command_state(Some(input( @@ -290,6 +308,8 @@ mod tests { ))); assert!(!running.start); assert!(running.stop); + assert!(running.pause); + assert!(!running.continue_service); assert!(!running.go_to_process); let verified = command_state(Some(input( @@ -298,6 +318,8 @@ mod tests { Some(ProcIdentity::new(44, 100)), ))); assert!(verified.stop); + assert!(!verified.pause); + assert!(verified.continue_service); assert!(verified.go_to_process); let pending = command_state(Some(input( @@ -307,9 +329,47 @@ mod tests { ))); assert!(!pending.start); assert!(!pending.stop); + assert!(!pending.pause); + assert!(!pending.continue_service); assert!(pending.go_to_process); } + #[test] + fn pause_and_continue_require_the_matching_state() { + assert!(control_reached_requested_state( + ServiceControlAction::Pause, + ServiceRunState::PausePending + )); + assert!(control_reached_requested_state( + ServiceControlAction::Pause, + ServiceRunState::Paused + )); + assert!(!control_reached_requested_state( + ServiceControlAction::Pause, + ServiceRunState::Running + )); + assert!(control_reached_requested_state( + ServiceControlAction::Continue, + ServiceRunState::ContinuePending + )); + assert!(control_reached_requested_state( + ServiceControlAction::Continue, + ServiceRunState::Running + )); + assert!(!control_reached_requested_state( + ServiceControlAction::Continue, + ServiceRunState::Paused + )); + assert_eq!( + classify_control_result(ServiceControlAction::Pause, Ok(ServiceRunState::Paused)), + Ok(ServiceRunState::Paused) + ); + assert_eq!( + classify_control_result(ServiceControlAction::Continue, Ok(ServiceRunState::Running)), + Ok(ServiceRunState::Running) + ); + } + #[test] fn control_success_requires_the_requested_state() { assert!(control_reached_requested_state( diff --git a/src/pages/services/sampler.rs b/src/pages/services/sampler.rs index 677b60c..d36d520 100644 --- a/src/pages/services/sampler.rs +++ b/src/pages/services/sampler.rs @@ -9,11 +9,11 @@ // 作者: OpenAI Codex // -------------------------------------------------------------------------- -//! Service Control Manager queries and start/stop calls. +//! Service Control Manager queries and start, stop, 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 start or stop opens that one service with only -//! the right the action needs plus status query so the result can be checked. +//! 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. use std::collections::HashMap; use std::mem::{size_of, zeroed}; @@ -26,7 +26,8 @@ use windows_sys::Win32::System::Services::{ CloseServiceHandle, ControlService, ENUM_SERVICE_STATUS_PROCESSW, EnumServicesStatusExW, OpenSCManagerW, OpenServiceW, QueryServiceConfig2W, QueryServiceConfigW, QueryServiceStatusEx, SC_ENUM_PROCESS_INFO, SC_MANAGER_CONNECT, SC_MANAGER_ENUMERATE_SERVICE, SC_STATUS_PROCESS_INFO, - SERVICE_CONFIG_DESCRIPTION, SERVICE_CONTROL_STOP, SERVICE_DESCRIPTIONW, SERVICE_QUERY_CONFIG, + SERVICE_CONFIG_DESCRIPTION, SERVICE_CONTROL_CONTINUE, SERVICE_CONTROL_PAUSE, + SERVICE_CONTROL_STOP, SERVICE_DESCRIPTIONW, SERVICE_PAUSE_CONTINUE, SERVICE_QUERY_CONFIG, SERVICE_QUERY_STATUS, SERVICE_START, SERVICE_STATE_ALL, SERVICE_STATUS, SERVICE_STATUS_PROCESS, SERVICE_STOP, SERVICE_WIN32, StartServiceW, }; @@ -470,25 +471,31 @@ 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::Pause | ServiceControlAction::Continue => { + SERVICE_PAUSE_CONTINUE | SERVICE_QUERY_STATUS + } }; let service = open_service(manager.as_raw(), service_name, access)?; - match action { + let control = 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 } - ServiceControlAction::Stop => { - let mut status = unsafe { zeroed::() }; - // SAFETY: the service is open for SERVICE_STOP. `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(), SERVICE_CONTROL_STOP, &mut status) }; - if success == 0 { - return Err(last_error()); - } + 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()); } } query_service_state(service.as_raw()) diff --git a/src/ui/dialogs.rs b/src/ui/dialogs.rs index d7d7cc2..ec1687f 100644 --- a/src/ui/dialogs.rs +++ b/src/ui/dialogs.rs @@ -1054,8 +1054,18 @@ fn build_services_dialog() -> DialogSpec<'static> { cx: 376, cy: 131, }, - button("&Start", IDM_SERVICE_START as i32, 0, 214, 144, 50, 14), - button("&Stop", IDM_SERVICE_STOP as i32, 0, 268, 144, 50, 14), + button("&Start", IDM_SERVICE_START as i32, 0, 98, 144, 50, 14), + button("&Stop", IDM_SERVICE_STOP as i32, 0, 152, 144, 50, 14), + button("&Pause", IDM_SERVICE_PAUSE as i32, 0, 206, 144, 50, 14), + button( + "&Continue", + IDM_SERVICE_CONTINUE as i32, + 0, + 260, + 144, + 58, + 14, + ), button( "&Go To Process", IDM_SERVICE_GOTOPROCESS as i32, diff --git a/src/ui/localization/text_key.rs b/src/ui/localization/text_key.rs index 620f5fa..cbf01ec 100644 --- a/src/ui/localization/text_key.rs +++ b/src/ui/localization/text_key.rs @@ -316,6 +316,8 @@ pub enum TextKey { ServicesMenu, ServiceStart, ServiceStop, + ServicePause, + ServiceContinue, ServiceColumnName, ServiceColumnDescription, ServiceColumnStartupType, @@ -333,6 +335,8 @@ pub enum TextKey { ServiceStartupDisabled, ServiceStartFailed, ServiceStopFailed, + ServicePauseFailed, + ServiceContinueFailed, ServiceStateUnchanged, ServiceProcessPid, DisconnectStatusHelp, diff --git a/src/ui/localization/ui.rs b/src/ui/localization/ui.rs index a3dc86a..be612f7 100644 --- a/src/ui/localization/ui.rs +++ b/src/ui/localization/ui.rs @@ -95,6 +95,12 @@ pub fn localize_dialog(hwnd: HWND, dialog_id: u16) { IDD_SERVICESPAGE => { set_dialog_item_text(hwnd, i32::from(IDM_SERVICE_START), TextKey::ServiceStart); set_dialog_item_text(hwnd, i32::from(IDM_SERVICE_STOP), TextKey::ServiceStop); + set_dialog_item_text(hwnd, i32::from(IDM_SERVICE_PAUSE), TextKey::ServicePause); + set_dialog_item_text( + hwnd, + i32::from(IDM_SERVICE_CONTINUE), + TextKey::ServiceContinue, + ); set_dialog_item_text( hwnd, i32::from(IDM_SERVICE_GOTOPROCESS), diff --git a/src/ui/menus.rs b/src/ui/menus.rs index 6b2a295..25513b6 100644 --- a/src/ui/menus.rs +++ b/src/ui/menus.rs @@ -289,6 +289,8 @@ 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_PAUSE, TextKey::ServicePause)?; + append_item(&mut menu, IDM_SERVICE_CONTINUE, TextKey::ServiceContinue)?; append_separator(&mut menu)?; append_item(&mut menu, IDM_SERVICE_GOTOPROCESS, TextKey::GoToProcess)?; Ok(menu) diff --git a/src/ui/resource_ids.rs b/src/ui/resource_ids.rs index 137fafb..651e6b7 100644 --- a/src/ui/resource_ids.rs +++ b/src/ui/resource_ids.rs @@ -372,6 +372,8 @@ pub const IDM_TASK_FINDPROCESS: u16 = 40047; pub const IDM_SERVICE_START: u16 = 40120; 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_PROC_TREE: u16 = 40130; pub const IDM_PROC_EXPAND: u16 = 40131; pub const IDM_PROC_COLLAPSE: u16 = 40132;