Skip to content

[Robustness] IServiceManager — async lifecycle methods (Start/Stop/Restart/Install) lack CancellationToken while Uninstall and read methods accept one #871

@Christophe-Rogiers

Description

@Christophe-Rogiers

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.

Metadata

Metadata

Assignees

Labels

acceptedIntended to be worked onenhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions