FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add Get-Service, Start-Service, Stop-Service and Restart-Service cmdlets on Unix by soroshsabz · Pull Request #28385 · PowerShell/PowerShell · GitHub

Repository navigation

Add Get-Service, Start-Service, Stop-Service and Restart-Service cmdlets on Unix - #28385

Open
soroshsabz wants to merge 12 commits into
PowerShell:masterfrom
soroshsabz:pr/unix-service-cmdlets
Open

soroshsabz wants to merge 12 commits into
PowerShell:masterfrom
soroshsabz:pr/unix-service-cmdlets

Conversation

soroshsabz commented Nov 10, 2026 •
edited
Loading

Copy link
Copy Markdown

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):

  • a fallback system to search for common commands used by various distros (including macOS) to restart a service on those platforms and if no common command is found then fail the entire operation
  • looking if sudo exists and using that to avoid the inevitable permission problem as sudo cannot elevate cmdlets
  • check the exit code and if non-zero, emit STDERR as an ErrorRecord
  • We would expect Get-Service, Stop-Service and Start-Service to also use the same/similar code

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:

Priority Manager Covered init systems
1 systemctl systemd
2 launchctl launchd (macOS)
3 service sysvinit and compatible
4 rc-service OpenRC

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:

  • Wildcards, -DisplayName, -Include/-Exclude, -PassThru, -WhatIf/-Confirm and pipeline
    input (Get-Service foo | Stop-Service) work as on Windows.
  • On systemd, unit names are accepted with or without the .service suffix; other managers use
    the provided name verbatim.
  • -DependentServices / -RequiredServices are implemented via systemctl list-dependencies;
    other service managers return no results.
  • Stop-Service -NoWait skips the wait for the stopped state; the stop request itself is unchanged.
  • -Force is accepted for script compatibility; service managers handle dependency ordering
    themselves.
  • Set-Service, New-Service, Remove-Service, Suspend-Service and Resume-Service remain
    Windows-only, per the WG recommendation that capabilities beyond these four cmdlets belong in a
    separate module.

Supporting changes

  • The four cmdlets are exported by the Unix Microsoft.PowerShell.Management module manifest.
  • The gsv, sasv and spsv aliases are now defined on Unix (they do not conflict with native
    commands there).
  • New neutral resource strings in ServiceResources.resx.

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):

Total: 22  Passed: 20  Failed: 0  Skipped: 2

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

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.
soroshsabz requested a review from a team as a code owner November 10, 2026 11:09
Copilot AI balanced review requested due to automatic review settings November 10, 2026 11:09

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copy link
Copy Markdown
Author

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).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copilot review overview

5 open issues What changed in this PR

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:

  • Introduces a Unix implementation of service cmdlets with service-manager discovery (systemctl/launchctl/service/rc-service) and sudo wrapping for mutating operations.
  • Exports the new cmdlets and adds Unix-only aliases (gsv, sasv, spsv).
  • Adds Pester coverage for Unix behavior via PATH shims and new resource strings for consistent error messaging.
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

🟡 Changes recommended

OpenRC enumeration invokes rc-service --all instead of rc-status --all, causing enumeration to fail before named service operations can run.

3 open issues 11 previously missed issues

In code that hasn't changed since last review

Map systemd reloading state to Running

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.

Map systemd disabled units to Manual

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.

Suppress systemd unit markers when parsing list-units

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.

Preserve launchd bootstrap domains for service mutations

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.

Stop KeepAlive launchd jobs without automatic restart

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.

Capture the process reference before checking or killing it

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.

Apply service suffix handling only to systemd

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.

Use case-sensitive comparisons for Unix service identity

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.

Resolve explicit systemd names outside the enumeration snapshot

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.

Load systemd descriptions before filtering by display name

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).

Query actual service state for PassThru results

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 review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

🟡 Changes recommended

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 issues

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

{
Dbg.Assert(!WildcardPattern.ContainsWildcardCharacters(nameOfService), "Caller should verify that nameOfService doesn't contain wildcard characters");

string name = UnixServiceParser.RemoveServiceSuffix(nameOfService.Trim());

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

string name = UnixServiceParser.RemoveServiceSuffix(nameOfService.Trim());
foreach (UnixServiceController service in AllServices)
{
if (string.Equals(service.Name, name, StringComparison.OrdinalIgnoreCase))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.)

}
}

return null;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

bool found = false;
foreach (UnixServiceController service in AllServices)
{
if (!wildcard.IsMatch(service.DisplayName))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Comment on lines +402 to +405
if (currentProcess is not null && !currentProcess.HasExited)
{
currentProcess.Kill(entireProcessTree: true);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Comment on lines +467 to +470
string domain = manager.IsElevated
? "system"
: $"gui/{UnixServiceNativeMethods.GetUserId()}";
string unitPath = domain + "/" + service.Name;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

return action switch
{
ServiceAction.Start => new List<string> { "kickstart", unitPath },
ServiceAction.Stop => new List<string> { "kill", "SIGTERM", unitPath },

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

private List<UnixServiceController> EnumerateSystemdServices()
{
ServiceCommandResult units = RunServiceManagerCommand(
new[] { "list-units", "--type=service", "--all", "--no-legend", "--no-pager" },

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

/// <returns>The service with the expected status.</returns>
internal static UnixServiceController ApplyExpectedStatus(UnixServiceController service, ServiceControllerStatus status)
{
service.Status = status;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

return unitFileState switch
{
"enabled" or "enabled-runtime" or "generated" or "generated-runtime" or "transient" => ServiceStartMode.Automatic,
"disabled" or "masked" => ServiceStartMode.Disabled,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Fixed in 0483d24 and 0637796: disabled now maps to ServiceStartMode.Manual (a disabled unit can still be started manually), masked/masked-runtime map to ServiceStartMode.Disabled, and the test expectation in Service.Tests.ps1 was updated accordingly.

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.
soroshsabz requested a balanced review from Copilot November 11, 2026 20:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restart-Service should call systemctl restart on Linux OSes with SystemD present.

2 participants


Back | FazBrowse Home | New Git URL