Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -610,146 +610,160 @@ public string[] Name
/// </summary>
protected override void ProcessRecord()
{
foreach (ServiceController service in MatchingServices())
nint scManagerHandle = nint.Zero;
if (!DependentServices && !RequiredServices)
{
if (!DependentServices.IsPresent && !RequiredServices.IsPresent)
{
WriteObject(AddProperties(service));
// As Get-Service only works on local services we get this once
// to retrieve extra properties added by PowerShell.
scManagerHandle = NativeMethods.OpenSCManagerW(
lpMachineName: null,
lpDatabaseName: null,
dwDesiredAccess: NativeMethods.SC_MANAGER_CONNECT);
if (scManagerHandle == nint.Zero)
{
Win32Exception exception = new();
string message = StringUtil.Format(ServiceResources.FailToOpenServiceControlManager, exception.Message);
ServiceCommandException serviceException = new ServiceCommandException(message, exception);
ErrorRecord err = new ErrorRecord(
serviceException,
"FailToOpenServiceControlManager",
ErrorCategory.PermissionDenied,
null);
ThrowTerminatingError(err);
}
else
}

try
{
foreach (ServiceController service in MatchingServices())
{
if (DependentServices.IsPresent)
if (!DependentServices.IsPresent && !RequiredServices.IsPresent)
{
foreach (ServiceController dependantserv in service.DependentServices)
WriteObject(AddProperties(scManagerHandle, service));
}
else
{
if (DependentServices.IsPresent)
{
WriteObject(dependantserv);
foreach (ServiceController dependantserv in service.DependentServices)
{
WriteObject(dependantserv);
}
}
}

if (RequiredServices.IsPresent)
{
foreach (ServiceController servicedependedon in service.ServicesDependedOn)
if (RequiredServices.IsPresent)
{
WriteObject(servicedependedon);
foreach (ServiceController servicedependedon in service.ServicesDependedOn)
{
WriteObject(servicedependedon);
}
}
}
}
}
finally
{
if (scManagerHandle != nint.Zero)
{
bool succeeded = NativeMethods.CloseServiceHandle(scManagerHandle);
Diagnostics.Assert(succeeded, "SCManager handle close failed");
}
}
}

#endregion Overrides

#nullable enable
/// <summary>
/// Adds UserName, Description, BinaryPathName, DelayedAutoStart and StartupType to a ServiceController object.
/// </summary>
/// <param name="scManagerHandle">Handle to the local SCManager instance.</param>
/// <param name="service"></param>
/// <returns>ServiceController as PSObject with UserName, Description and StartupType added.</returns>
private PSObject AddProperties(ServiceController service)
private static PSObject AddProperties(nint scManagerHandle, ServiceController service)
{
NakedWin32Handle hScManager = IntPtr.Zero;
NakedWin32Handle hService = IntPtr.Zero;
Comment thread
jborean93 marked this conversation as resolved.
Outdated
int lastError = 0;
PSObject serviceAsPSObj = PSObject.AsPSObject(service);

// As these are optional values, a failure due to permissions or
// other problem is ignored and the properties are set to null.
bool isDelayedAutoStart = false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does it make sense to have the default value for isDelayedAutoStart to be null too? In case we failed to retrieve the actual value for this property, we probably should not use false as the default value.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I can make it nullable, it just means that we would need to decide how GetServiceStartupType treats the value as null. We could either

  • Treated as false
  • Treated the whole start type as invalid (or when Automatic)

The former is simpler but it may not be true if the service is truly automatic but delayed and we couldn't get the correct result.

https://github.com/PowerShell/PowerShell/blob/27e8cb3c9212d1d3e9a14198cbe822e7f7aec9dc/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs#L2915-L2931

Copy link
Copy Markdown
Member

@daxian-dbw daxian-dbw Sep 9, 2024

Choose a reason for hiding this comment

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

Given that startupType depends on the real value of autostartInfo.fDelayedAutostart, if we cannot get the real value of isDelayedAutoStart I think we may have to treat the startupType as invalid.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Has been updated to keep it as InvalidValue (what it was if the config couldn't be retrieved) if we couldn't get the automatic delayed start value.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

isDelayedAutoStart is not updated to have a null value when it cannot be retrieved.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

string? binPath = null;
string? description = null;
string? startName = null;
ServiceStartupType startupType = ServiceStartupType.InvalidValue;
try
{
hScManager = NativeMethods.OpenSCManagerW(
lpMachineName: service.MachineName,
lpDatabaseName: null,
dwDesiredAccess: NativeMethods.SC_MANAGER_CONNECT
);
if (hScManager == IntPtr.Zero)
{
lastError = Marshal.GetLastWin32Error();
Win32Exception exception = new(lastError);
WriteNonTerminatingError(
service,
exception,
"FailToOpenServiceControlManager",
ServiceResources.FailToOpenServiceControlManager,
ErrorCategory.PermissionDenied);
}

// We don't use service.ServiceHandle as that requests
// SERVICE_ALL_ACCESS when we only need SERVICE_QUERY_CONFIG.
hService = NativeMethods.OpenServiceW(
hScManager,
scManagerHandle,
service.ServiceName,
NativeMethods.SERVICE_QUERY_CONFIG
);
if (hService == IntPtr.Zero)
if (hService != nint.Zero)
{
lastError = Marshal.GetLastWin32Error();
Win32Exception exception = new(lastError);
WriteNonTerminatingError(
service,
exception,
"CouldNotGetServiceInfo",
ServiceResources.CouldNotGetServiceInfo,
ErrorCategory.PermissionDenied);
}

NativeMethods.SERVICE_DESCRIPTIONW description = new();
bool querySuccessful = NativeMethods.QueryServiceConfig2<NativeMethods.SERVICE_DESCRIPTIONW>(hService, NativeMethods.SERVICE_CONFIG_DESCRIPTION, out description);

NativeMethods.SERVICE_DELAYED_AUTO_START_INFO autostartInfo = new();
querySuccessful = querySuccessful && NativeMethods.QueryServiceConfig2<NativeMethods.SERVICE_DELAYED_AUTO_START_INFO>(hService, NativeMethods.SERVICE_CONFIG_DELAYED_AUTO_START_INFO, out autostartInfo);
if (NativeMethods.QueryServiceConfig2(
hService,
NativeMethods.SERVICE_CONFIG_DESCRIPTION,
out NativeMethods.SERVICE_DESCRIPTIONW descriptionInfo))
{
description = descriptionInfo.lpDescription;
}

NativeMethods.QUERY_SERVICE_CONFIG serviceInfo = new();
querySuccessful = querySuccessful && NativeMethods.QueryServiceConfig(hService, out serviceInfo);
if (NativeMethods.QueryServiceConfig2(
hService,
NativeMethods.SERVICE_CONFIG_DELAYED_AUTO_START_INFO,
out NativeMethods.SERVICE_DELAYED_AUTO_START_INFO autostartInfo))
{
isDelayedAutoStart = autostartInfo.fDelayedAutostart;
}

if (!querySuccessful)
{
WriteNonTerminatingError(
service: service,
innerException: null,
errorId: "CouldNotGetServiceInfo",
errorMessage: ServiceResources.CouldNotGetServiceInfo,
category: ErrorCategory.PermissionDenied
);
if (NativeMethods.QueryServiceConfig(
hService,
out NativeMethods.QUERY_SERVICE_CONFIG serviceInfo))
{
binPath = serviceInfo.lpBinaryPathName;
startName = serviceInfo.lpServiceStartName;
startupType = NativeMethods.GetServiceStartupType(
Comment thread
daxian-dbw marked this conversation as resolved.
Outdated
(ServiceStartMode)serviceInfo.dwStartType,
isDelayedAutoStart);
}
}

PSProperty noteProperty = new("UserName", serviceInfo.lpServiceStartName);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#UserName");

noteProperty = new PSProperty("Description", description.lpDescription);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#Description");

noteProperty = new PSProperty("DelayedAutoStart", autostartInfo.fDelayedAutostart);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#DelayedAutoStart");

noteProperty = new PSProperty("BinaryPathName", serviceInfo.lpBinaryPathName);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#BinaryPathName");

noteProperty = new PSProperty("StartupType", NativeMethods.GetServiceStartupType(service.StartType, autostartInfo.fDelayedAutostart));
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#StartupType");
}
finally
{
if (hService != IntPtr.Zero)
{
bool succeeded = NativeMethods.CloseServiceHandle(hService);
if (!succeeded)
{
Diagnostics.Assert(lastError != 0, "ErrorCode not success");
}
}

if (hScManager != IntPtr.Zero)
{
bool succeeded = NativeMethods.CloseServiceHandle(hScManager);
if (!succeeded)
{
Diagnostics.Assert(lastError != 0, "ErrorCode not success");
}
Diagnostics.Assert(succeeded, "Failed to close service handle");
}
}

PSObject serviceAsPSObj = PSObject.AsPSObject(service);
PSNoteProperty noteProperty = new("UserName", startName);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#UserName");

noteProperty = new PSNoteProperty("Description", description);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#Description");

noteProperty = new PSNoteProperty("DelayedAutoStart", isDelayedAutoStart);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#DelayedAutoStart");

noteProperty = new PSNoteProperty("BinaryPathName", binPath);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#BinaryPathName");

noteProperty = new PSNoteProperty("StartupType", startupType);
serviceAsPSObj.Properties.Add(noteProperty, true);
serviceAsPSObj.TypeNames.Insert(0, "System.Service.ServiceController#StartupType");
Comment thread
daxian-dbw marked this conversation as resolved.
Outdated

return serviceAsPSObj;
}
}
#nullable disable
#endregion GetServiceCommand

#region ServiceOperationBaseCommand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,6 @@
<data name="CouldNotSetService" xml:space="preserve">
<value>Service '{1} ({0})' cannot be configured due to the following error: {2}</value>
</data>
<data name="CouldNotGetServiceInfo" xml:space="preserve">
<value>Service '{1} ({0})' cannot be queried due to the following error: {2}</value>
</data>
<data name="CouldNotSetServiceDescription" xml:space="preserve">
<value>Service '{1} ({0})' description cannot be configured due to the following error: {2}</value>
</data>
Expand Down Expand Up @@ -211,7 +208,7 @@
<value>Service '{1} ({0})' resume failed.</value>
</data>
<data name="FailToOpenServiceControlManager" xml:space="preserve">
<value>Failed to configure the service '{1} ({0})' due to the following error: {2}. Run PowerShell as admin and run your command again.</value>
<value>Failed to open SCManager due to the following error: {0}. Run PowerShell as admin and run your command again.</value>
</data>
<data name="UnsupportedStartupType" xml:space="preserve">
<value>The startup type '{0}' is not supported by {1}.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,60 @@ Describe "Get-Service cmdlet tests" -Tags "CI" {
{ & $script } | Should -Throw -ErrorId $errorid
}
}

Describe 'Get-Service Admin tests' -Tag CI,RequireAdminOnWindows {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if ( -not $IsWindows ) {
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}

BeforeEach {
$serviceParams = @{
Name = "PowerShellTest-$([Guid]::NewGuid().Guid)"
BinaryPathName = "$env:SystemRoot\System32\cmd.exe"
StartupType = 'Manual'
}
$service = New-Service @serviceParams
}
AfterEach {
$service | Remove-Service
}

It "Ignores invalid description MUI entry" {
$serviceRegPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$($service.Name)"
Set-ItemProperty -LiteralPath $serviceRegPath -Name Description -Value '@Fake.dll,-0'
$actual = Get-Service -Name $service.Name -ErrorAction Stop

$actual.Name | Should -Be $service.Name
$actual.Status | Should -Be Stopped
$actual.Description | Should -BeNullOrEmpty
$actual.UserName | Should -Be LocalSystem
$actual.StartupType | Should -Be Manual
}

It "Ignores no SERVICE_QUERY_CONFIG access" {
$sddl = ((sc.exe sdshow $service.Name) -join "").Trim()
$sd = ConvertFrom-SddlString -Sddl $sddl
$sd.RawDescriptor.DiscretionaryAcl.AddAccess(
[System.Security.AccessControl.AccessControlType]::Deny,
[System.Security.Principal.WindowsIdentity]::GetCurrent().User,
0x1, # SERVICE_QUERY_CONFIG
[System.Security.AccessControl.InheritanceFlags]::None,
[System.Security.AccessControl.PropagationFlags]::None)
$newSddl = $sd.RawDescriptor.GetSddlForm([System.Security.AccessControl.AccessControlSections]::All)
$null = sc.exe sdset $service.Name $newSddl

$actual = Get-Service -Name $service.Name -ErrorAction Stop

$actual.Name | Should -Be $service.Name
$actual.Status | Should -Be Stopped
$actual.Description | Should -BeNullOrEmpty
$actual.UserName | Should -BeNullOrEmpty
$actual.StartupType | Should -Be InvalidValue
}
}