Severity: Warning
File: src/Servy.Core/Services/IServiceManager.cs
The interface mixes cancellation support inconsistently:
| Method |
Async? |
CancellationToken? |
InstallServiceAsync |
yes |
no |
UninstallServiceAsync |
yes |
yes |
StartServiceAsync |
yes |
no |
StopServiceAsync |
yes |
no |
RestartServiceAsync |
yes |
no |
GetServiceStatus |
sync |
yes |
GetServiceStartupType |
sync |
yes |
GetAllServices |
sync |
yes |
Task<OperationResult> InstallServiceAsync(InstallServiceOptions options);
Task<OperationResult> UninstallServiceAsync(string serviceName, CancellationToken cancellationToken = default);
Task<OperationResult> StartServiceAsync(string serviceName, bool logSuccessfulStart = true);
Task<OperationResult> StopServiceAsync(string serviceName, bool logSuccessfulStop = true);
Task<OperationResult> RestartServiceAsync(string serviceName);
ServiceControllerStatus GetServiceStatus(string serviceName, CancellationToken cancellationToken = default);
The lifecycle methods are exactly the ones that can take minutes in the real world — SCM RPC hangs, services in StopPending limbo, services that never reach Running, hardware drivers misbehaving on stop. Yet they have no way for callers to time out or signal Ctrl+C. The Manager UI's Stop button can't actually be cancelled if SCM stalls — it stays "in progress" forever.
Issue #819 already calls out one symptom of this: ServiceManager.GetAllServices's Parallel.ForEach has no per-service timeout. The same root pattern applies to the entire async write surface.
Domain Service.cs:376-401 exposes the same shape to callers:
public async Task<OperationResult> Start() => await _serviceManager.StartServiceAsync(Name);
public virtual async Task<OperationResult> Stop() => await _serviceManager.StopServiceAsync(Name);
public async Task<OperationResult> Restart() => await _serviceManager.RestartServiceAsync(Name);
public ServiceControllerStatus? GetStatus(CancellationToken cancellationToken = default) { ... }
public async Task<OperationResult> Uninstall(CancellationToken cancellationToken = default) { ... }
So Start/Stop/Restart cannot accept a token even from the domain layer.
Suggested fix:
Add CancellationToken cancellationToken = default to all five async lifecycle methods on the interface, and propagate to the SCM polling loops inside ServiceManager:
Task<OperationResult> InstallServiceAsync(InstallServiceOptions options, CancellationToken cancellationToken = default);
Task<OperationResult> StartServiceAsync(string serviceName, bool logSuccessfulStart = true, CancellationToken cancellationToken = default);
Task<OperationResult> StopServiceAsync(string serviceName, bool logSuccessfulStop = true, CancellationToken cancellationToken = default);
Task<OperationResult> RestartServiceAsync(string serviceName, CancellationToken cancellationToken = default);
Inside ServiceManager the polling loops (e.g. while (sc.Status != Stopped) patterns) already have a Task.Delay that can take the token; just thread it through. Domain/Service.cs Start/Stop/Restart should then accept and forward the token, matching the existing Uninstall(CancellationToken) signature.
Bonus side-effect: the Manager UI can drive these from await ExecuteLockedAsync(... ct ...) and surface a real "Cancel" affordance on operations that hang.
Severity: Warning
File:
src/Servy.Core/Services/IServiceManager.csThe interface mixes cancellation support inconsistently:
InstallServiceAsyncUninstallServiceAsyncStartServiceAsyncStopServiceAsyncRestartServiceAsyncGetServiceStatusGetServiceStartupTypeGetAllServicesThe lifecycle methods are exactly the ones that can take minutes in the real world — SCM RPC hangs, services in StopPending limbo, services that never reach Running, hardware drivers misbehaving on stop. Yet they have no way for callers to time out or signal Ctrl+C. The Manager UI's
Stopbutton can't actually be cancelled if SCM stalls — it stays "in progress" forever.Issue #819 already calls out one symptom of this:
ServiceManager.GetAllServices'sParallel.ForEachhas no per-service timeout. The same root pattern applies to the entire async write surface.Domain
Service.cs:376-401exposes the same shape to callers:So
Start/Stop/Restartcannot accept a token even from the domain layer.Suggested fix:
Add
CancellationToken cancellationToken = defaultto all five async lifecycle methods on the interface, and propagate to the SCM polling loops insideServiceManager:Inside
ServiceManagerthe polling loops (e.g.while (sc.Status != Stopped)patterns) already have aTask.Delaythat can take the token; just thread it through.Domain/Service.csStart/Stop/Restart should then accept and forward the token, matching the existingUninstall(CancellationToken)signature.Bonus side-effect: the Manager UI can drive these from
await ExecuteLockedAsync(... ct ...)and surface a real "Cancel" affordance on operations that hang.