| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Implements PowerShell#18863 as reviewed by the cmdlets WG: - New Unix-only Service.cs implementation (#if UNIX) that discovers the service manager in this order: systemctl (systemd), launchctl (macOS), service (sysvinit), rc-service (OpenRC), and fails the entire operation with a terminating error when none is found. - Start/Stop/Restart wrap the service manager with 'sudo -n' when not running as root (sudo cannot elevate cmdlets), and report a clear error when a password would be required. - Non-zero exit codes surface the command stderr as a non-terminating ErrorRecord per service, with error IDs mirroring the Windows implementation. - Get-Service returns UnixServiceController objects with Name, DisplayName, Status, StartType, Description, SubState and UnitFileState; Status/StartType reuse the System.ServiceProcess enums for pipeline compatibility with Windows. Wildcards, -DisplayName, -Include/-Exclude and -PassThru behave like Windows. -DependentServices/-RequiredServices are supported on systemd. - Stop-Service -NoWait maps to 'systemctl --no-block'; -Force is accepted for script compatibility (service managers order services themselves). - Exports the four cmdlets in the Unix Microsoft.PowerShell.Management module manifest and enables the gsv/sasv/spsv aliases on Unix. - Adds Service.Tests.ps1 exercising the real command resolution and invocation paths against PATH shims for each service manager.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Sorry, something went wrong.
|
Companion documentation update: MicrosoftDocs/PowerShell-Docs#13383 (documents the Unix behavior, the new UnixServiceController output type, and parameter differences in the 7.7 version folder). |
Sorry, something went wrong.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds cross-platform service management support by implementing Get-Service, Start-Service, Stop-Service, and Restart-Service on Unix platforms, aligning behavior and error semantics with Windows where practical.
Changes:
| File | Description |
|---|---|
| test/powershell/Modules/Microsoft.PowerShell.Management/Service.Tests.ps1 | Adds Pester tests for Unix service cmdlets using service-manager and sudo shims. |
| src/System.Management.Automation/engine/InitialSessionState.cs | Adds Unix-only aliases for the new service cmdlets. |
| src/Modules/Unix/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1 | Exports the service cmdlets from the Unix Management module. |
| src/Microsoft.PowerShell.Commands.Management/resources/ServiceResources.resx | Adds new resource strings for Unix service-manager and sudo/error scenarios. |
| src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs | Implements Unix service cmdlets, parsing, invocation, sudo wrapping, and output shaping. |
Review effort: Lite
Sorry, something went wrong.
There was a problem hiding this comment.
OpenRC enumeration invokes rc-service --all instead of rc-status --all, causing enumeration to fail before named service operations can run.
3 open issuesIn code that hasn't changed since last review
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:623
reloading is a systemd ActiveState for a service that is still running, but it currently falls through to Stopped. A Get-Service query during systemctl reload therefore incorrectly reports the live service as stopped and breaks status-based filtering. Map reloading to Running as well.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:640
A systemd disabled unit is still startable manually or through dependencies/socket activation; it is not equivalent to Windows ServiceStartMode.Disabled. Mapping it to Disabled misclassifies ordinary manually-started services in cross-platform StartType filters. Map disabled to Manual, and reserve Disabled for masked/masked-runtime; update the test that currently expects Disabled for foo.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:755
systemctl list-units can prefix failed/not-found units with a ● marker even with --no-legend. The parser assumes the first token is the unit name, so it discards those rows. Failed transient services disappear entirely, and file-backed ones lose their failure substate/description when recreated from list-unit-files. Add --plain to suppress the marker and cover a failed-unit row in the tests.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:841
A non-root macOS process does not necessarily have a gui/<uid> bootstrap domain. In a headless/SSH session, launchctl list can enumerate jobs in user/<uid>, but every mutation here targets the nonexistent GUI domain and fails even with working sudo. Preserve or resolve the service's actual bootstrap domain instead of choosing it solely from root status and UID.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:852
Sending SIGTERM to a launchd job does not keep it stopped. For a job configured with KeepAlive, launchd starts another process immediately, yet Stop-Service reports success (and -PassThru reports Stopped). Implement launchd lifecycle handling that actually leaves the job stopped and allows a subsequent Start-Service to restore it; test this with a KeepAlive job rather than only asserting the signal arguments.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:1165
StopProcessing runs concurrently with the invocation thread, whose finally block sets _currentProcess to null. The repeated field reads can therefore pass the null check and then dereference null in HasExited or Kill; the existing catches do not handle that exception. Capture the process reference once before checking and killing it.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:1449
Applying systemd's .service convention to every backend makes exact lookups miss labels such as com.example.service, while wildcard operations can select unintended services through synthesized names. Restrict both suffix removal here and the alternative suffixed match in MatchesServiceName at lines 1688–1690 to systemd.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:1452
Unix service names can differ only by case, so this lookup can make Stop-Service foo stop Foo instead; case-insensitive identity maps also merge distinct services or omit dependencies. Use ordinal, case-sensitive comparisons for service identity here and in the dictionaries, deduplication, and self-dependency filter at lines 365, 527, 1642, 1993, and 2001.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:1461
An explicit systemd name is rejected unless it already appears in enumeration. A valid but never-started template instance such as worker@blue.service appears in neither list-units nor list-unit-files (which lists worker@.service, deliberately filtered out here), so Start-Service worker@blue reports not found although systemctl can start it. Resolve exact names directly through systemd, validating LoadState, rather than treating the enumeration snapshot as exhaustive.
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:1567
Services found only in list-unit-files have DisplayName set to their unit name. Only GetOneService refreshes their description, so Get-Service -DisplayName 'Example Worker' cannot find an unloaded service even though Get-Service worker retrieves that display name successfully. Refresh missing metadata before applying the display-name filter (preferably with a batched systemd query).
src/Microsoft.PowerShell.Commands.Management/commands/management/ServiceUnix.cs:1819
A successful manager exit does not establish this status. In particular, Stop-Service -NoWait -PassThru always returns Stopped even though --no-block only queues the stop, and a successful start of a oneshot unit can leave it inactive. This also leaves SubState inconsistent with Status. Query the actual service state for PassThru instead of overwriting the snapshot with the requested end state.
5 resolved since last reviewReview effort: Balanced
Sorry, something went wrong.
There was a problem hiding this comment.
Unresolved service-identity bugs can target unintended Unix services, while non-root tests contain guaranteed assertion failures and can invoke the host’s real sudo.
5 open issuesReview effort: Balanced
Sorry, something went wrong.
There was a problem hiding this comment.
Case-insensitive identity handling and backend-agnostic suffix stripping can stop the wrong service, while unresolved enumeration, state-reporting, and lifecycle defects further compromise service management.
10 open issuesReview effort: Balanced
Sorry, something went wrong.
| { | ||
| Dbg.Assert(!WildcardPattern.ContainsWildcardCharacters(nameOfService), "Caller should verify that nameOfService doesn't contain wildcard characters"); | ||
|
|
||
| string name = UnixServiceParser.RemoveServiceSuffix(nameOfService.Trim()); |
There was a problem hiding this comment.
Fixed in 0637796: the facade strips the .service suffix only when the manager is systemd; launchd, SysV and OpenRC names are used verbatim, so Stop-Service -Name com.example.service on launchd targets the exact label. There is also no synthetic suffix alternative in wildcard matching anymore - the shared master code matches wildcards against the manager-reported name, and systemd enumeration names are suffix-stripped by the parser.
Sorry, something went wrong.
| string name = UnixServiceParser.RemoveServiceSuffix(nameOfService.Trim()); | ||
| foreach (UnixServiceController service in AllServices) | ||
| { | ||
| if (string.Equals(service.Name, name, StringComparison.OrdinalIgnoreCase)) |
There was a problem hiding this comment.
Fixed in 0483d24 and 0637796: the facade now prefers an exact ordinal name match and only falls back to a case-insensitive comparison when no exact match exists, and the parser's unit-file merge and OpenRC deduplication dictionaries compare names ordinally, so distinct units differing only in case are no longer conflated. (The duplicate detection in master's MultipleServiceCommandBase already compared names case-sensitively.)
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| return null; |
There was a problem hiding this comment.
Acknowledged: template instances that were never loaded (for example openvpn@client.service) appear in neither list-units nor list-unit-files (only the openvpn@.service template is listed, and template instances are deliberately not synthesized), so an exact-name lookup reports NoServiceFoundForGivenName. Adding a targeted systemctl show <unit> existence check for enumeration misses is a possible follow-up; it is out of scope for this working-group PR, which focuses on basic Get/Start/Stop/Restart parity.
Sorry, something went wrong.
| bool found = false; | ||
| foreach (UnixServiceController service in AllServices) | ||
| { | ||
| if (!wildcard.IsMatch(service.DisplayName)) |
There was a problem hiding this comment.
True: systemctl list-unit-files does not carry descriptions, so for units that were never loaded the facade exposes the unit name as DisplayName, and -DisplayName matching by description only works for loaded units (which come from list-units with a description). Populating descriptions for never-loaded units would require a metadata query (for example systemctl show) per unit; that cost is not justified for the working-group scope of this PR, and it is noted as a possible follow-up.
Sorry, something went wrong.
| if (currentProcess is not null && !currentProcess.HasExited) | ||
| { | ||
| currentProcess.Kill(entireProcessTree: true); | ||
| } |
There was a problem hiding this comment.
This code path no longer exists in the PR: the parallel-implementation files (including ServiceBaseCommand.cs with the currentProcess field and the custom StopProcessing) were removed in the rework, and master's ServiceBaseCommand is reused unchanged. Resolving as obsolete.
Sorry, something went wrong.
| string domain = manager.IsElevated | ||
| ? "system" | ||
| : $"gui/{UnixServiceNativeMethods.GetUserId()}"; | ||
| string unitPath = domain + "/" + service.Name; |
There was a problem hiding this comment.
The facade enumerates with launchctl list in the caller's context and derives the mutation target from the same identity (the system domain for root, gui/<uid> otherwise), so enumeration and operations always address the same domain. Per-job domain discovery would require parsing launchctl print output for every job; that is beyond the working-group scope of this PR and is noted as a follow-up.
Sorry, something went wrong.
| return action switch | ||
| { | ||
| ServiceAction.Start => new List<string> { "kickstart", unitPath }, | ||
| ServiceAction.Stop => new List<string> { "kill", "SIGTERM", unitPath }, |
There was a problem hiding this comment.
Acknowledged. Switching stop to bootout would prevent KeepAlive relaunches, but the facade only knows the job's label from launchctl list; re-starting a booted-out job requires bootstrap with the job's plist path, which is not tracked. The current label-based lifecycle (kickstart / kill SIGTERM / kickstart -k) operates on exactly the jobs that enumeration reported and is straightforwardly sudo-wrappable. Discovering each job's plist path (for example from launchctl print) so that stop can use bootout and start can use bootstrap is a reasonable follow-up beyond the working-group scope of this PR.
Sorry, something went wrong.
| private List<UnixServiceController> EnumerateSystemdServices() | ||
| { | ||
| ServiceCommandResult units = RunServiceManagerCommand( | ||
| new[] { "list-units", "--type=service", "--all", "--no-legend", "--no-pager" }, |
There was a problem hiding this comment.
Fixed in 0637796: --plain was added to the list-units invocation so failed units are no longer prefixed with a marker, and the test now enumerates a failed unit and asserts its name, status and sub-state.
Sorry, something went wrong.
| /// <returns>The service with the expected status.</returns> | ||
| internal static UnixServiceController ApplyExpectedStatus(UnixServiceController service, ServiceControllerStatus status) | ||
| { | ||
| service.Status = status; |
There was a problem hiding this comment.
The Unix facade deliberately keeps Status as the state reported by the last enumeration or operation: mutating operations update it from the manager's exit status, and when waiting is requested the shared cmdlet code (DoWaitForStatus) confirms the transition. With -NoWait the caller explicitly opts out of waiting, so a point-in-time snapshot is the documented behavior; SubState likewise reflects the last enumeration. Note also that the referenced file (ServiceOperationBaseCommand.cs) no longer exists in this PR - master's implementation is reused unchanged and the Unix behavior lives in UnixServiceController.cs. Querying the live state for -NoWait -PassThru would add manager invocations and special-case oneshot units, so I would like to keep that as a possible follow-up.
Sorry, something went wrong.
| return unitFileState switch | ||
| { | ||
| "enabled" or "enabled-runtime" or "generated" or "generated-runtime" or "transient" => ServiceStartMode.Automatic, | ||
| "disabled" or "masked" => ServiceStartMode.Disabled, |
Sorry, something went wrong.
Compile the master Service.cs cmdlet implementation on Unix as well and map the unqualified ServiceController name to a Unix service-manager facade (UnixServiceController) via a using alias, so Get/Start/Stop/ Restart-Service work on Unix with only a few platform-specific branches: - Service.cs: top '#if !UNIX' replaced by Unix aliases; the five Windows-SCM-only cmdlets (Set/New/Remove/Suspend/Resume-Service) and the SCM property/native sections stay guarded with '#if !UNIX'. - Get-Service ProcessRecord gains a Unix branch that honors -DependentServices/-RequiredServices and reports NoServiceManagerFound. - DoStartService/DoStopService surface sudo-unavailable failures as SudoRequired errors on Unix. - UnixServiceNativeMethods gains the ERROR_SERVICE_* constants that the shared code compares against, aliased as NativeMethods on Unix.
…ching - 'systemctl list-units' gains --plain so failed units (prefixed with a marker without it) are still enumerated. - systemd 'disabled' maps to ServiceStartMode.Manual (it can still be started manually); ServiceStartMode.Disabled is reserved for masked unit files, matching the Windows semantics of these values. - The Unix service controller prefers an exact native name match and only falls back to the case-insensitive lookup when there is no exact match, so distinct units differing in case are not conflated. - The '.service' suffix is only stripped for the systemd manager; other managers use the provided name verbatim.
| Back | FazBrowse Home | New Git URL |
PR Summary
This PR implements Get-Service, Start-Service, Stop-Service, and Restart-Service on Unix
(Linux and macOS), based on issue
These cmdlets previously did not exist on Unix at all, which made cross-platform service
management impossible.
The implementation follows the cmdlets WG recommendation recorded in
#18863 (comment):
Rather than duplicating the cmdlets, master's Service.cs is now also compiled on Unix. A
using ServiceController = Microsoft.PowerShell.Commands.UnixServiceController alias maps the
unqualified ServiceController name to a new Unix facade (UnixServiceController.cs) so the
shared cmdlet implementation works unchanged. Only a few places are platform-specific: the five
Windows-SCM-only cmdlets (Set-Service, New-Service, Remove-Service, Suspend-Service,
Resume-Service) and the SCM property/native sections stay guarded with #if !UNIX,
Get-Service's ProcessRecord has a small Unix branch, and DoStartService/DoStopService
surface sudo-unavailable failures as SudoRequired errors.
PR Context
Fixes #18863
Service manager fallback chain
The cmdlets resolve the service manager through PATH in this order and fail the operation with a
terminating error when none is found:
Elevation
Mutating operations (Start-Service, Stop-Service, Restart-Service) are wrapped with
sudo -n when the process is not running as root, so they never hang on a password prompt. If
sudo would require a password, the cmdlet writes a non-terminating error with guidance to run
pwsh as root or configure passwordless sudo. Get-Service works unprivileged.
Error semantics
Non-zero exit codes surface the command's stderr as a non-terminating ErrorRecord per service
(exception type ServiceCommandException, mirroring Windows), with error IDs matching the Windows
cmdlets (CouldNotStartService, CouldNotStopService), plus SudoRequired when elevation is
unavailable. A missing service manager is reported as a terminating NoServiceManagerFound error,
and service-manager enumeration failures terminate the cmdlet.
Output type
Get-Service returns the new Microsoft.PowerShell.Commands.UnixServiceController type with
Name, DisplayName, Status, StartType, Description, SubState, and UnitFileState.
Status and StartType reuse the System.ServiceProcess.ServiceControllerStatus /
ServiceStartMode enums (the managed assembly ships cross-platform; only the Windows
implementation is RID-specific), so Where-Object Status -eq Running and other pipelines behave
identically on both platforms. Objects carry a DefaultDisplayPropertySet so the default table
view matches Windows.
Parameter behavior:
input (Get-Service foo | Stop-Service) work as on Windows.
the provided name verbatim.
other service managers return no results.
themselves.
Windows-only, per the WG recommendation that capabilities beyond these four cmdlets belong in a
separate module.
Supporting changes
commands there).
Notes
Tests
New test/powershell/Modules/Microsoft.PowerShell.Management/Service.Tests.ps1 covers systemd
enumeration, name/display-name/wildcard matching, dependencies, the exact service-manager
invocation arguments for systemd, launchctl, sysvinit and OpenRC (including sudo wrapping), the
sudo error paths, and the no-service-manager and enumeration-failure errors. The manager
behaviors are exercised through command shims, so the suite does not require a real systemd
installation.
All 22 tests pass against a local Linux build of this PR (run in WSL Ubuntu as root; the 2
sudo-specific tests skip as root and run on non-root agents):
The Windows build is unchanged by this PR (#if !UNIX sections compile as before, verified by a
Windows cross-compile of the affected project).
Breaking Changes & Documentation
This is not a breaking change: the Windows behavior of all service cmdlets is unchanged. The
Resolve-Path-style documentation for the four cross-platform cmdlets has been updated to
describe the Unix support:
PR Checklist