diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs
index 5a3a8b6e8b3..568d1fe44ad 100644
--- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs
+++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs
@@ -1601,7 +1601,7 @@ protected override void ProcessRecord()
bool objServiceShouldBeDisposed = false;
try
{
- if (_ParameterSetName.Equals("InputObject", StringComparison.OrdinalIgnoreCase) && InputObject != null)
+ if (InputObject != null)
{
service = InputObject;
Name = service.ServiceName;
@@ -2167,6 +2167,195 @@ protected override void BeginProcessing()
} // class NewServiceCommand
#endregion NewServiceCommand
+ #region RemoveServiceCommand
+ ///
+ /// This class implements the Remove-Service command
+ ///
+ [Cmdlet(VerbsCommon.Remove, "Service", SupportsShouldProcess = true, DefaultParameterSetName = "Name")]
+ public class RemoveServiceCommand : ServiceBaseCommand
+ {
+ #region Parameters
+
+ ///
+ /// Name of the service to remove
+ ///
+ [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Name")]
+ [Alias("ServiceName", "SN")]
+ public string Name { get; set; }
+
+ ///
+ /// The following is the definition of the input parameter "InputObject".
+ /// Specifies ServiceController object representing the services to be removed.
+ /// Enter a variable that contains the objects or type a command or expression
+ /// that gets the objects.
+ ///
+ [Parameter(ValueFromPipeline = true, ParameterSetName = "InputObject")]
+ public ServiceController InputObject { get; set; }
+
+ ///
+ /// The following is the definition of the input parameter "ComputerName".
+ /// Set the properties of service running on the list of computer names
+ /// specified. The default is the local computer.
+ /// Type the NETBIOS name, an IP address, or a fully-qualified domain name of
+ /// one or more remote computers. To indicate the local computer, use the
+ /// computer name, "localhost" or a dot (.). When the computer is in a different
+ /// domain than the user, the fully-qualified domain name is required.
+ ///
+ [Parameter(ValueFromPipelineByPropertyName = true)]
+ [ValidateNotNullOrEmpty]
+ [Alias("cn")]
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
+ public String[] ComputerName { get; set; } = new string[] { "." };
+
+ #endregion Parameters
+
+ #region Overrides
+ ///
+ /// Remove the service
+ ///
+ [ArchitectureSensitive]
+ protected override void ProcessRecord()
+ {
+ ServiceController service = null;
+ string serviceComputerName = null;
+ foreach (string computer in ComputerName)
+ {
+ bool objServiceShouldBeDisposed = false;
+ try
+ {
+ if (InputObject != null)
+ {
+ service = InputObject;
+ Name = service.ServiceName;
+ serviceComputerName = service.MachineName;
+ objServiceShouldBeDisposed = false;
+ }
+ else
+ {
+ serviceComputerName = computer;
+ // "new ServiceController" will succeed even if there is no such service.
+ // This checks whether the service actually exists.
+ service = new ServiceController(Name, serviceComputerName);
+ objServiceShouldBeDisposed = true;
+ }
+ Diagnostics.Assert(!String.IsNullOrEmpty(Name), "null ServiceName");
+ string unusedByDesign = service.DisplayName;
+ }
+ catch (ArgumentException ex)
+ {
+ // Cannot use WriteNonterminatingError as service is null
+ ErrorRecord er = new ErrorRecord(ex, "ArgumentException", ErrorCategory.ObjectNotFound, computer);
+ WriteError(er);
+ continue;
+ }
+ catch (InvalidOperationException ex)
+ {
+ // Cannot use WriteNonterminatingError as service is null
+ ErrorRecord er = new ErrorRecord(ex, "InvalidOperationException", ErrorCategory.ObjectNotFound, computer);
+ WriteError(er);
+ continue;
+ }
+
+ try // In finally we ensure dispose, if object not pipelined.
+ {
+ // Confirm the operation first.
+ // This is always false if WhatIf is set.
+ if (!ShouldProcessServiceOperation(service))
+ {
+ continue;
+ }
+
+ NakedWin32Handle hScManager = IntPtr.Zero;
+ NakedWin32Handle hService = IntPtr.Zero;
+ try
+ {
+ hScManager = NativeMethods.OpenSCManagerW(
+ lpMachineName: serviceComputerName,
+ lpDatabaseName: null,
+ dwDesiredAccess: NativeMethods.SC_MANAGER_ALL_ACCESS
+ );
+ if (IntPtr.Zero == hScManager)
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ Win32Exception exception = new Win32Exception(lastError);
+ WriteObject(exception);
+ WriteNonTerminatingError(
+ service,
+ serviceComputerName,
+ exception,
+ "ComputerAccessDenied",
+ ServiceResources.ComputerAccessDenied,
+ ErrorCategory.PermissionDenied);
+ continue;
+ }
+ hService = NativeMethods.OpenServiceW(
+ hScManager,
+ Name,
+ NativeMethods.SERVICE_DELETE
+ );
+ if (IntPtr.Zero == hService)
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ Win32Exception exception = new Win32Exception(lastError);
+ WriteNonTerminatingError(
+ service,
+ exception,
+ "CouldNotRemoveService",
+ ServiceResources.CouldNotSetService,
+ ErrorCategory.PermissionDenied);
+ continue;
+ }
+
+ bool status = NativeMethods.DeleteService(hService);
+
+ if (!status)
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ Win32Exception exception = new Win32Exception(lastError);
+ WriteNonTerminatingError(
+ service,
+ exception,
+ "CouldNotRemoveService",
+ ServiceResources.CouldNotRemoveService,
+ ErrorCategory.PermissionDenied);
+ }
+ }
+ finally
+ {
+ if (IntPtr.Zero != hService)
+ {
+ bool succeeded = NativeMethods.CloseServiceHandle(hService);
+ if (!succeeded)
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ Diagnostics.Assert(lastError != 0, "ErrorCode not success");
+ }
+ }
+
+ if (IntPtr.Zero != hScManager)
+ {
+ bool succeeded = NativeMethods.CloseServiceHandle(hScManager);
+ if (!succeeded)
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ Diagnostics.Assert(lastError != 0, "ErrorCode not success");
+ }
+ }
+ } // Finally
+ } // End try
+ finally
+ {
+ if (objServiceShouldBeDisposed)
+ {
+ service.Dispose();
+ }
+ }
+ }// End for
+ }
+ #endregion Overrides
+ } // class RemoveServiceCommand
+ #endregion RemoveServiceCommand
+
#region ServiceCommandException
///
/// Non-terminating errors occurring in the service noun commands
@@ -2264,8 +2453,10 @@ internal static class NativeMethods
internal const int ERROR_SERVICE_NOT_ACTIVE = 1062;
internal const DWORD SC_MANAGER_CONNECT = 1;
internal const DWORD SC_MANAGER_CREATE_SERVICE = 2;
+ internal const DWORD SC_MANAGER_ALL_ACCESS = 0xf003f;
internal const DWORD SERVICE_QUERY_CONFIG = 1;
internal const DWORD SERVICE_CHANGE_CONFIG = 2;
+ internal const DWORD SERVICE_DELETE = 0x10000;
internal const DWORD SERVICE_NO_CHANGE = 0xffffffff;
internal const DWORD SERVICE_AUTO_START = 0x2;
internal const DWORD SERVICE_DEMAND_START = 0x3;
@@ -2297,6 +2488,12 @@ bool CloseServiceHandle(
NakedWin32Handle hSCManagerOrService
);
+ [DllImport(PinvokeDllNames.DeleteServiceDllName, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern
+ bool DeleteService(
+ NakedWin32Handle hService
+ );
+
[DllImport(PinvokeDllNames.ChangeServiceConfigWDllName, CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern
bool ChangeServiceConfigW(
diff --git a/src/Microsoft.PowerShell.Commands.Management/resources/ServiceResources.resx b/src/Microsoft.PowerShell.Commands.Management/resources/ServiceResources.resx
index 60ecdd041f0..8c94d5bb35a 100644
--- a/src/Microsoft.PowerShell.Commands.Management/resources/ServiceResources.resx
+++ b/src/Microsoft.PowerShell.Commands.Management/resources/ServiceResources.resx
@@ -171,6 +171,9 @@
Service '{1} ({0})' was created, but its description cannot be configured due to the following error: {2}
+
+ Service '{1} ({0})' cannot be removed due to the following error: {2}
+
'Cannot access dependent services of '{1} ({0})'
diff --git a/src/Modules/Windows-Core/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1 b/src/Modules/Windows-Core/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1
index 9a8bdff1565..1807ae22e70 100644
--- a/src/Modules/Windows-Core/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1
+++ b/src/Modules/Windows-Core/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1
@@ -56,6 +56,7 @@ CmdletsToExport=@("Add-Content",
"Restart-Service",
"Set-Service",
"New-Service",
+ "Remove-Service",
"Set-Content",
"Set-ItemProperty",
"Test-Connection",
diff --git a/src/Modules/Windows-Full/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1 b/src/Modules/Windows-Full/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1
index 6325b6fe97d..b2024a3651b 100644
--- a/src/Modules/Windows-Full/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1
+++ b/src/Modules/Windows-Full/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1
@@ -68,6 +68,7 @@ CmdletsToExport=@("Add-Content",
"Restart-Service",
"Set-Service",
"New-Service",
+ "Remove-Service",
"Set-Content",
"Set-ItemProperty",
"Set-WmiInstance",
diff --git a/src/System.Management.Automation/utils/PInvokeDllNames.cs b/src/System.Management.Automation/utils/PInvokeDllNames.cs
index 33d21c45983..619349b888f 100644
--- a/src/System.Management.Automation/utils/PInvokeDllNames.cs
+++ b/src/System.Management.Automation/utils/PInvokeDllNames.cs
@@ -135,5 +135,6 @@ internal static class PinvokeDllNames
internal const string Process32FirstDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*121*/
internal const string Process32NextDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*122*/
internal const string GetACPDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*123*/
+ internal const string DeleteServiceDllName = "api-ms-win-service-management-l1-1-0.dll"; /*124*/
}
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Set-Service.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Set-Service.Tests.ps1
index 354daff452a..cf78e8ba69a 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Management/Set-Service.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Set-Service.Tests.ps1
@@ -1,4 +1,4 @@
-Describe "Set/New-Service cmdlet tests" -Tags "Feature", "RequireAdminOnWindows" {
+Describe "Set/New/Remove-Service cmdlet tests" -Tags "Feature", "RequireAdminOnWindows" {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if ( -not $IsWindows ) {
@@ -195,6 +195,65 @@ Describe "Set/New-Service cmdlet tests" -Tags "Feature", "RequireAdminOnWindows"
}
}
+ It "Remove-Service can remove a service" {
+ try {
+ $servicename = "testremoveservice"
+ $parameters = @{
+ Name = $servicename;
+ BinaryPathName = "$PSHOME\powershell.exe"
+ }
+ $service = New-Service @parameters
+ $service | Should Not BeNullOrEmpty
+ Remove-Service -Name $servicename
+ $service = Get-Service -Name $servicename -ErrorAction SilentlyContinue
+ $service | Should BeNullOrEmpty
+ }
+ finally {
+ Get-CimInstance Win32_Service -Filter "name='$servicename'" | Remove-CimInstance -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "Remove-Service can accept pipeline input of a ServiceController" {
+ try {
+ $servicename = "testremoveservice"
+ $parameters = @{
+ Name = $servicename;
+ BinaryPathName = "$PSHOME\powershell.exe"
+ }
+ $service = New-Service @parameters
+ $service | Should Not BeNullOrEmpty
+ Get-Service -Name $servicename | Remove-Service
+ $service = Get-Service -Name $servicename -ErrorAction SilentlyContinue
+ $service | Should BeNullOrEmpty
+ }
+ finally {
+ Get-CimInstance Win32_Service -Filter "name='$servicename'" | Remove-CimInstance -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "Remove-Service cannot accept a service that does not exist" {
+ { Remove-Service -Name "testremoveservice" -ErrorAction 'Stop' } | ShouldBeErrorId "InvalidOperationException,Microsoft.PowerShell.Commands.RemoveServiceCommand"
+ }
+
+ It "Set-Service can accept pipeline input of a ServiceController" {
+ try {
+ $servicename = "testsetservice"
+ $newdisplayname = "newdisplayname"
+ $parameters = @{
+ Name = $servicename;
+ BinaryPathName = "$PSHOME\powershell.exe"
+ }
+ $service = New-Service @parameters
+ $service | Should Not BeNullOrEmpty
+ Get-Service -Name $servicename | Set-Service -DisplayName $newdisplayname
+ $service = Get-Service -Name $servicename
+ $service.DisplayName | Should BeExactly $newdisplayname
+ }
+ finally {
+ Get-CimInstance Win32_Service -Filter "name='$servicename'" | Remove-CimInstance -ErrorAction SilentlyContinue
+ }
+ }
+
It "Using bad parameters will fail for '' where '' = ''" -TestCases @(
@{cmdlet="New-Service"; name = 'credtest' ; parameter = "Credential" ; value = (
[System.Management.Automation.PSCredential]::new("username",
diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
index 50ddb81b4d6..7c5d70f3fce 100644
--- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
+++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
@@ -374,6 +374,7 @@ Describe "Verify approved aliases list" -Tags "CI" {
"Cmdlet", "Remove-PSDrive", , $($FullCLR -or $CoreWindows -or $CoreUnix)
"Cmdlet", "Remove-PSSession", , $($FullCLR -or $CoreWindows -or $CoreUnix)
"Cmdlet", "Remove-PSSnapin", , $($FullCLR )
+"Cmdlet", "Remove-Service", , $($FullCLR -or $CoreWindows )
"Cmdlet", "Remove-TypeData", , $($FullCLR -or $CoreWindows -or $CoreUnix)
"Cmdlet", "Remove-Variable", , $($FullCLR -or $CoreWindows -or $CoreUnix)
"Cmdlet", "Remove-WmiObject", , $($FullCLR )