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 )