Carbon.Windows.Service.psm1
|
using namespace System.Security.AccessControl using namespace System.Security.Principal using namespace System.ServiceProcess # Copyright WebMD Health Services # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License #Requires -Version 5.1 Set-StrictMode -Version 'Latest' # Functions should use $script:moduleDirPath as the relative root from which to find things. A published module has its # function appended to this file, while a module in development has its functions in the Functions directory. $script:moduleDirPath = $PSScriptRoot $script:numaEnabled = $null # $script:quotesEmptyStringArgs = $null $modulesDirPath = Join-Path -Path $PSScriptRoot -ChildPath 'M' -Resolve Import-Module -Name (Join-Path -Path $modulesDirPath -ChildPath 'PureInvoke\PureInvoke.psm1' -Resolve) ` -Function @( 'Invoke-AdvApiOpenSCManager', 'Invoke-AdvApiOpenService', 'Invoke-AdvApiCloseServiceHandle', 'Invoke-AdvApiQueryServiceConfig', 'Invoke-AdvApiQueryServiceConfig2', 'Invoke-AdvApiQueryServiceObjectSecurity', 'Invoke-AdvApiSetServiceObjectSecurity', 'Write-Win32Error' ) ` -Verbose:$false Import-Module -Name (Join-Path -Path $modulesDirPath -ChildPath 'Carbon.Accounts\Carbon.Accounts.psm1' -Resolve) ` -Function @('Resolve-CPrincipal') ` -Verbose:$false Import-Module -Name (Join-Path -Path $modulesDirPath -ChildPath 'Carbon.Security\Carbon.Security.psm1' -Resolve) ` -Function @('Grant-CPrivilege') ` -Verbose:$false Import-Module -Name (Join-Path -Path $modulesDirPath -ChildPath 'Carbon.FileSystem\Carbon.FileSystem.psm1' -Resolve) ` -Function @('Grant-CNtfsPermission') ` -Verbose:$false [Flags()] enum Carbon_Windows_Service_ServiceAccessRights { QueryConfig = 0x00001 ChangeConfig = 0x00002 QueryStatus = 0x00004 EnumerateDependents = 0x00008 Start = 0x00010 Stop = 0x00020 PauseContinue = 0x00040 Interrogate = 0x00080 UserDefinedControl = 0x00100 Delete = 0x10000 ReadControl = 0x20000 WriteDac = 0x40000 WriteOwner = 0x80000 FullControl = 0xf01ff } enum Carbon_Windows_Service_FailureAction { None = 0 Restart = 1 Reboot = 2 RunCommand = 3 } # Classes are cached by PowerShell. To support different versions of a class loaded side-by-side in the same PowerShell # session, need to have a version number in the name. class Carbon_Windows_Service_ServiceAccessRule_v1 : AccessRule { Carbon_Windows_Service_ServiceAccessRule_v1([IdentityReference] $identity, [Carbon_Windows_Service_ServiceAccessRights] $rights, [AccessControlType] $type) : base($identity, [int]$rights, $false, [InheritanceFlags]::None, [PropagationFlags]::None, $type) { $this.ServiceAccessRights = $rights } [Carbon_Windows_Service_ServiceAccessRights] $ServiceAccessRights [bool] Equals([Object] $obj) { if ($null -eq $obj) { return $false } if ($obj -isnot [Carbon_Windows_Service_ServiceAccessRule_v1]) { return $false } return $obj.ServiceAccessRights -eq $this.ServiceAccessRights -and ` $obj.IdentityReference -eq $this.IdentityReference -and ` $obj.AccessControlType -eq $this.AccessControlType } # https://github.com/microsoft/referencesource/blob/main/mscorlib/system/tuple.cs#L52-L55 [int] CombineHashCodes([int] $h1, [int] $h2) { return (($h1 -shl 5) + $h1) -bxor $h2 } [int] GetHashCode() { $h1 = $this.ServiceAccessRights.GetHashCode() $h2 = $this.IdentityReference.GetHashCode() $h3 = $this.AccessControlType.GetHashCode() $h = $this.CombineHashCodes($h1, $h2) return $this.CombineHashCodes($h, $h3) } } # Store each of your module's functions in its own file in the Functions directory. On the build server, your module's # functions will be appended to this file, so only dot-source files that exist on the file system. This allows # developers to work on a module without having to build it first. Grab all the functions that are in their own files. $functionsPath = Join-Path -Path $script:moduleDirPath -ChildPath 'Functions\*.ps1' if( (Test-Path -Path $functionsPath) ) { foreach( $functionPath in (Get-Item $functionsPath) ) { . $functionPath.FullName } } function Assert-CService { <# .SYNOPSIS Checks if a service exists, and writes an error if it doesn't. .DESCRIPTION Also returns `True` if the service exists, `False` if it doesn't. .OUTPUTS System.Boolean. .LINK Test-CService .EXAMPLE Assert-CService -Name 'Drivetrain' Writes an error if the `Drivetrain` service doesn't exist. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] # The name of the service. $Name ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if( -not (Test-CService $Name) ) { Write-Error ('Service {0} not found.' -f $Name) return $false } return $true } function Get-CServiceAcl { <# .SYNOPSIS Gets the discretionary access control list (i.e. DACL) for a service. .DESCRIPTION You wanted it, you got it! You probably want to use `Get-CServicePermission` instead. If you want to chagne a service's permissions, use `Grant-CServicePermission` or `Revoke-ServicePermissions`. .LINK Get-CServicePermission .LINK Grant-CServicePermission .LINK Revoke-CServicePermission .EXAMPLE Get-CServiceAcl -Name Hyperdrive Gets the `Hyperdrive` service's DACL. #> [CmdletBinding()] [OutputType([Security.AccessControl.DiscretionaryAcl])] param( [Parameter(Mandatory=$true)] [string] # The service whose DACL to return. $Name ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $rawSD = Get-CServiceSecurityDescriptor -Name $Name ,[DiscretionaryAcl]::New($false, $false, $rawSD.DiscretionaryAcl) } function Get-CServiceConfiguration { <# .SYNOPSIS Gets a service's full configuration, e.g. username, path, failure actions, etc. .DESCRIPTION The `Get-CServiceConfiguration` function gets service configuration information. It uses the Windows API's `QueryServiceConfig` and `QueryServiceConfig2` functions. Pass the name of the service to the `Name` parameter. That service's full configuration is returned. You can also pipe `[ServiceProcess.ServiceController]` objects (e.g., the output of the `Get-Service` cmdlet). Returned objects have the following properties: | Name | Description | Windows API Structure/Property | | ------------------------------- | ---------------------------------- | ------------------------------ | | `[string] Name` | Name | | | `[Enum] ServiceType` | Type | [`QUERY_SERVICE_CONFIGW.dwServiceType`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[Enum] StartType` | When to start. | [`QUERY_SERVICE_CONFIGW.dwStartType`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[Enum] ErrorControl` | Startup error handling. | [`QUERY_SERVICE_CONFIGW.dwErrorControl`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[string] Path` | Executable path, with arguments. | [`QUERY_SERVICE_CONFIGW.lpBinaryPathName`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[string] LoadOrderGroup` | Load ordering group. | [`QUERY_SERVICE_CONFIGW.lpLoadOrderGroup`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[uint] TagID` | Unique tag. | [`QUERY_SERVICE_CONFIGW.dwTagId`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[string[]] Dependencies` | Names of dependencies. | [`QUERY_SERVICE_CONFIGW.lpDependencies`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[string] UserName` | User/Identity/Principal name | [`QUERY_SERVICE_CONFIGW.lpServiceStartName`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[string] DisplayName` | Display name | [`QUERY_SERVICE_CONFIGW.lpDisplayName`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-query_service_configw) | | `[bool] DelayedAutoStart` | Starts automatically, but delayed? | [`SERVICE_DELAYED_AUTO_START_INFO.fDelayedAutostart`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_delayed_auto_start_info) | | `[string] Description` | Description | [`SERVICE_DESCRIPTIONW.lpDescription`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_descriptionw) | | `[TimeSpan] FailureResetPeriod` | Failure reset frequency. | [`SERVICE_FAILURE_ACTIONSW.dwResetPeriod`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actionsw) | | `[string] FailureRebootMessage` | Reboot message to send to users. | [`SERVICE_FAILURE_ACTIONSW.lpRebootMsg`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actionsw) | | `[string] FailureCommand` | Command for "run command" failure actions. | [`SERVICE_FAILURE_ACTIONSW.lpCommand`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actionsw) | | `[Object[]] FailureActions` | Failure actions. | [`SC_ACTION`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-sc_action) | | `[bool] FailureActionsOnNonCrashFailures` | Shutdown error handling. | [`SERVICE_FAILURE_ACTIONS_FLAG.fFailureActionsOnNonCrashFailures`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actions_flag) | | `[ushort] PreferredNode` | Preferred NUMA node. | [`SERVICE_PREFERRED_NODE_INFO.usPreferredNode`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_preferred_node_info) | | `[TimeSpan] PreshutdownTimeout` | Shutdown timeout. | [`SERVICE_PRESHUTDOWN_INFO.dwPreshutdownTimeout`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_preshutdown_info) | | `[string[]] RequiredPrivileges` | Required privileges. | [`pmszRequiredPrivileges`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_required_privileges_infow) | | `[Enum] SidType` | SID type. | [`SERVICE_SID_INFO.dwServiceSidType`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_sid_info) | | `[Object] Triggers` | Trigger events. | [`SERVICE_TRIGGER`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger) | | `[Enum] ProtectionType` | Protection type. | [`SERVICE_LAUNCH_PROTECTED_INFO.dwLaunchProtected`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_launch_protected_info) | Each failure action has the following properties: | Name | Description | Windows API Structure/Property | | ------------------ | --------------------------- | ------------------------------ | | `[Enum] Type` | Type | [`SC_ACTION.Type`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-sc_action) | | `[TimeSpan] Delay` | Delay before taking action. | [`SC_ACTION.Delay`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-sc_action) | Each trigger has the following properties: | Name | Description | Windows API Structure/Property | | ---------------------- | ----------- | ------------------------------ | | `[Enum] Type` | Type | [`SERVICE_TRIGGER.dwTriggerType`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger) | | `[Enum] Action` | Action | [`SERVICE_TRIGGER.dwAction`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger) | | `[Guid] Subtype` | Subtype | [`SERVICE_TRIGGER.pTriggerSubtype`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger) | | `[Object[]] DataItems` | Trigger-specific data. | [`SERVICE_TRIGGER_SPECIFIC_DATA_ITEM`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger_specific_data_item) | Each trigger data item has the following properties: | Name | Description | Windows API Structure/Property | | --------------- | ---------------------- | ------------------------------ | | `[Enum] Type` | Type | [`SERVICE_TRIGGER_SPECIFIC_DATA_ITEM.dwDataType`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger_specific_data_item) | `[Object] Data` | Trigger-specific data. | [`SERVICE_TRIGGER_SPECIFIC_DATA_ITEM.pData`](https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger_specific_data_item) Ignore types for enums and objects. Those are implementation details and are subject to change at any time. Enum names/values shouldn't change. The user running this function must have `QueryConfig` permissions to the service. Use `Grant-CServicePermission` to grant service permissions. .LINK Grant-CServicePermission .EXAMPLE Get-Service | Get-CServiceConfiguration Demonstrates how you can pipe in a `ServiceController` object to load the service. This works for services on remote computers as well. .EXAMPLE Get-CServiceConfiguration -Name 'w3svc' Demonstrates how you can get a specific service's configuration. .EXAMPLE Get-CServiceConfiguration -Name 'w3svc' -ComputerName 'enterprise' Demonstrates how to get service configuration for a service on a remote computer. #> [CmdletBinding()] param( # The name of the service. Wildcards are *not* supported. You can pipe `[ServiceProcess.ServiceController]` # objects as well. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=0)] [String] $Name, # The name of the computer where the service lives. [Parameter(ValueFromPipelineByPropertyName)] [Alias('MachineName')] [String] $ComputerName ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $scmHandle = Invoke-AdvApiOpenSCManager -MachineName $ComputerName } process { $svcHandle = Invoke-AdvApiOpenService -SCManagerHandle $scmHandle -ServiceName $Name -DesiredAccess 'QueryConfig' if (-not $svcHandle) { return } $config = [ordered]@{} try { $config['Name'] = $Name $winSvcCfg = Invoke-AdvApiQueryServiceConfig -ServiceHandle $svcHandle if ($winSvcCfg) { $config['ServiceType'] = $winSvcCfg.ServiceType $config['StartType'] = $winSvcCfg.StartType $config['ErrorControl'] = $winSvcCfg.ErrorControl $config['Path'] = $winSvcCfg.BinaryPathName $config['LoadOrderGroup'] = $winSvcCfg.LoadOrderGroup $config['TagID'] = $winSvcCfg.TagID $config['Dependencies'] = $winSvcCfg.Dependencies $config['UserName'] = $winSvcCfg.ServiceStartName.Trim('"') $config['DisplayName'] = $winSvcCfg.DisplayName } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel DelayedAutoStart if ($winSvcCfg) { $config['DelayedAutoStart'] = $winSvcCfg.DelayedAutoStart } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel Description if ($winSvcCfg) { $config['Description'] = $winSvcCfg.Description } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel FailureActions if ($winSvcCfg) { $config['FailureResetPeriod'] = $null # 0xffffffff means INFINITE or not set if ($winSvcCfg.ResetPeriod -ne [UInt32]0xffffffffl) { $config['FailureResetPeriod'] = [TimeSpan]::New(0, 0, $winSvcCfg.ResetPeriod) } $config['FailureRebootMessage'] = $winSvcCfg.RebootMessage $config['FailureCommand'] = $winSvcCfg.Command [Object[]] $failureActions = $winSvcCfg.Actions | ForEach-Object { return [pscustomobject]@{ Type = $_.Type Delay = [TimeSpan]::New(0, 0, 0, 0, $_.Delay) } } if ($null -eq $failureActions) { $failureActions = [Object[]]::New(0) } $config['FailureActions'] = $failureActions } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel FailureActionsFlag if ($winSvcCfg) { $config['FailureActionsOnNonCrashFailures'] = $winSvcCfg.FailureActionsOnNonCrashFailures } $config['PreferredNode'] = $null if ($null -eq $script:numaEnabled -or $script:numaEnabled) { # If NUMA isn't enabled, querying PreferredNode results in a "The parameter is incorrect." (87) error. # This is the only way I've found to reliably detect if NUMA is enabled. $preferredNodeErrors = @() $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle ` -InfoLevel PreferredNode ` -ErrorAction SilentlyContinue ` -ErrorVariable 'preferredNodeErrors' if ($winSvcCfg) { $script:numaEnabled = $true $config['PreferredNode'] = $winSvcCfg.PreferredNode } else { $parameterIncorrect = 87 $paramIncorrectEx = $preferredNodeErrors | Select-Object -ExpandProperty 'Exception' -ErrorAction Ignore | Where-Object 'NativeErrorCode' -EQ $parameterIncorrect -ErrorAction Ignore if ($paramIncorrectEx) { $script:numaEnabled = $false for ($idx = 0 ; $idx -lt $preferredNodeErrors.Count ; $idx++) { $Global:Error.RemoveAt(0) } } } } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel Preshutdown if ($winSvcCfg) { $config['PreshutdownTimeout'] = [TimeSpan]::New(0, 0, 0, 0, $winSvcCfg.PreshutdownTimeout) } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel RequiredPrivileges if ($winSvcCfg) { if ($winSvcCfg.RequiredPrivileges) { $config['RequiredPrivileges'] = $winSvcCfg.RequiredPrivileges } else { $config['RequiredPrivileges'] = [String[]]::New(0) } } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel SidType if ($winSvcCfg) { $config['SidType'] = $winSvcCfg.SidType } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel Triggers if ($winSvcCfg) { $config['Triggers'] = $winSvcCfg.Triggers } $winSvcCfg = Invoke-AdvApiQueryServiceConfig2 -ServiceHandle $svcHandle -InfoLevel LaunchProtected if ($winSvcCfg) { $config['LaunchProtected'] = $winSvcCfg.LaunchProtected } return [pscustomobject]$config } finally { $svcHandle | Invoke-AdvApiCloseServiceHandle } } end { $scmHandle | Invoke-AdvApiCloseServiceHandle } } function Get-CServicePermission { <# .SYNOPSIS Gets the permissions for a service. .DESCRIPTION The `Get-CServicePermission` returns the permissions for a service. Pass the service's name to the `Name` parameter. It uses the Windows API's `QueryServiceObjectSecurity` function to get the discretionary ACL for the service. It converts each of the ACL's access control entries (ACE) into `[Security.AccessControl.AccessRule]` objects and returns them. Any system audit or alarm ACEs are skipped. Return objects will have a `ServiceAccessRights` property that is a flags enumeration of the permissions. To get the permissions for a specific principal, pass its name to the `PrincipalName` parameter. The type of the objects returned and the type of the `ServiceAccessRights` enum are an implementation detail and should be ignored. To access the service ACEs, use `Get-CServiceAcl`. .LINK Grant-ServicePermissions .LINK Revoke-ServicePermissions .LINK Get-CServiceAcl .LINK Set-CServiceACl .EXAMPLE Get-CServicePermission -Name 'Hyperdrive' Gets the access rules for the `Hyperdrive` service. .EXAMPLE Get-CServicePermission -Name 'Hyperdrive' -PrincipalName 'FALCON\HSolo' Gets just Han's permissions to control the `Hyperdrive` service. #> [CmdletBinding()] param( # The name of the service whose permissions to return. [Parameter(Mandatory)] [String] $Name, # The specific principal whose permissions to get. Wildcards *not* supported. [String] $PrincipalName ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $dacl = Get-CServiceAcl -Name $Name $principal = $null if ($PrincipalName) { $principal = Resolve-CPrincipal -Name $PrincipalName if( -not $principal ) { return } } $dacl | ForEach-Object { $ace = $_ $identity = $ace.SecurityIdentifier; if ($identity.IsValidTargetType([NTAccount])) { $numErrorsBefore = $Global:Error.Count try { $identity = $identity.Translate([NTAccount]) } catch [IdentityNotMappedException] { # user doesn't exist anymore. So sad. $numErrorsNow = $Global:Error.Count for ($idx = 0 ; $idx -lt ($numErrorsNow - $numErrorsBefore) ; $idx++) { $Global:Error.RemoveAt(0) } } } if ($ace.AceQualifier -eq [AceQualifier]::AccessAllowed) { $ruleType = [AccessControlType]::Allow } elseif ($ace.AceQualifier -eq [AceQualifier]::AccessDenied) { $ruleType = [AccessControlType]::Deny } else { $msg = "Get-CServicePermission: Service ${Name}: skipping unsupported $($ace.AceQualifier) ACE for " + "princial ""${identity}""." Write-Verbose $msg return } [Carbon_Windows_Service_ServiceAccessRule_v1]::New($identity, $ace.AccessMask, $ruleType) } | Where-Object { if( $principal ) { return ($_.IdentityReference.Value -eq $principal.FullName) } return $_ } } function Get-CServiceSecurityDescriptor { <# .SYNOPSIS Gets a Windows service's security descriptor. .DESCRIPTION The `Get-CServiceSecurityDescriptor` function gets a Windows service's security descriptor. Pass the service's name to the `Name` parameter (wildcards *not* accepted). The function uses the Windows API's `OpenSCManager`, `OpenService`, and `QueryServiceObjectSecurity` functions to get the service's security descriptor. Returns a `[Security.AccessControl.RawSecurityDescriptor]` object with `Owner`, `Group`, and `DiscretionaryAcl` set. The `SystemAcl` is not set. User must have `ReadControl` permission to a service. Even with that permission, some services still require elevated access. .OUTPUTS System.Security.AccessControl.RawSecurityDescriptor. .LINK Get-CServicePermission .LINK Grant-ServicePermissions .LINK Revoke-ServicePermissions .EXAMPLE Get-CServiceSecurityDescriptor -Name 'Hyperdrive' Gets the hyperdrive service's raw security descriptor. #> [CmdletBinding()] [OutputType([Security.AccessControl.RawSecurityDescriptor])] param( # The name of the service whose security descriptor to return. Wildcards *not* accepted. [Parameter(Mandatory)] [String] $Name ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $scmHandle = Invoke-AdvApiOpenSCManager -DesiredAccess Read if (-not $scmHandle) { return } try { $svcHandle = Invoke-AdvApiOpenService -SCManagerHandle $scmHandle -ServiceName $Name -DesiredAccess ReadControl if (-not $svcHandle) { return } try { Invoke-AdvApiQueryServiceObjectSecurity -ServiceHandle $svcHandle ` -SecurityInformation 'Owner, Group, DiscretionaryAcl' } finally { $svcHandle | Invoke-AdvApiCloseServiceHandle } } finally { $scmHandle | Invoke-AdvApiCloseServiceHandle } } function Grant-CServiceControlPermission { <# .SYNOPSIS Grants permission to control a Windows service. .DESCRIPTION The `Grant-CServiceControlPermission` grants a principal the permission to control a Windows service (i.e. to use PowerShell's service cmdlets to query, start, and stop the service). Pass the service name to the `Name` parameter and the principal's name to the `PrincipalName` parameter. The user is granted permission to control the service, replacing any existing permissions the principal has. By default, only Administrators are allowed to control a service. You may notice that when running the `Stop-Service`, `Start-Service`, or `Restart-Service` cmdlets as a non-Administrator, you get permissions errors. That's because you need to correct permissions. This function grants just the permissions needed to use PowerShell's `Stop-Service`, `Start-Service`, and `Restart-Service` cmdlets to control a service. .LINK Get-CServicePermission .LINK Grant-CServicePermission .LINK Revoke-CServicePermission .EXAMPLE Grant-CServiceControlPermission -ServiceName 'TPSReport' -PrincipalName 'INITRODE\Builders' Grants the INITRODE\Builders group permission to control the TPSReport service. #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')] param( # The name of the service. [Parameter(Mandatory)] [String] $Name, # The user/group name being given access. [Parameter(Mandatory)] [String] $PrincipalName ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState Grant-CServicePermission -Name $Name ` -PrincipalName $PrincipalName ` -Permission 'QueryStatus, EnumerateDependents, Start, Stop' } function Grant-CServicePermission { <# .SYNOPSIS Grants permissions to a service. .DESCRIPTION The `Grant-CServicePermission` function grants permissions to a service. Pass the service's name to the `Name` parameter, the principal's name to the `PrincipalName` parameter, and the permissions to grant to the `Permission` parameter. Valid permissions are: * QueryConfig * ChangeConfig * QueryStatus * EnumerateDependents * Start * Stop * PauseContinue * Interrogate * UserDefinedControl * Delete * ReadControl * WriteDac * WriteOwner * FullControl To grant multiple permissions, use a flags enum string, e.g. `'QueryConfig, QueryStatus, EnumerateDependents'`. By default, only Administators are allowed to manage a service. Use this function to grant other principals permissions to manage a service. If you just want to grant a user the ability to start/stop/restart a service using PowerShell's `Start-Service`, `Stop-Service`, or `Restart-Service` cmdlets, use the `Grant-ServiceControlPermission` function instead. Any previous permissions are replaced. .LINK Get-CServicePermission .LINK Grant-ServiceControlPermission .EXAMPLE Grant-CServicePermission -Identity FALCON\Chewbacca -Name Hyperdrive 'QueryStatus, EnumerateDependents, Start, Stop' Grants Chewbacca the permissions to query, enumerate dependents, start, and stop the `Hyperdrive` service. Coincedentally, these are the permissions that Chewbacca nees to run `Start-Service`, `Stop-Service`, `Restart-Service`, and `Get-Service` cmdlets against the `Hyperdrive` service. #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')] param( # The name of the service to grant permissions to. [Parameter(Mandatory)] [String] $Name, # The principal to grant permissions to. [Parameter(Mandatory)] [String] $PrincipalName, # The permissions to grant. Valid values are: # # * QueryConfig # * ChangeConfig # * QueryStatus # * EnumerateDependents # * Start # * Stop # * PauseContinue # * Interrogate # * UserDefinedControl # * Delete # * ReadControl # * WriteDac # * WriteOwner # * FullControl # # To grant multiple permissions, use a flags enum string, e.g. `'QueryConfig, QueryStatus, EnumerateDependents'`. [Parameter(Mandatory)] [Carbon_Windows_Service_ServiceAccessRights] $Permission ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $account = Resolve-CPrincipal -Name $PrincipalName if( -not $account ) { return } if( -not (Assert-CService -Name $Name) ) { return } $perm = Get-CServicePermission -Name $Name -PrincipalName $PrincipalName if ($perm -and $perm.ServiceAccessRights -eq $Permission) { $msg = "[Grant-CServicePermission] ""$($account.FullName)"" already has ""${Permission}"" permission to " + """${Name} service." Write-Verbose $msg return } $msg = "[Grant-CServicePermission] Granting ""$($account.FullName)"" ""${Permission}"" permission to the " + """${Name}"" service." Write-Information $msg $dacl = Get-CServiceAcl -Name $Name $dacl.SetAccess( [Security.AccessControl.AccessControlType]::Allow, $account.Sid, $Permission, 'None', 'None' ) Set-CServiceAcl -Name $Name -DACL $dacl } function Install-CService { <# .SYNOPSIS Installs a Windows service. .DESCRIPTION `Install-CService` uses `sc.exe` to install a Windows service. If a service with the given name already exists, it is stopped, its configuration is updated to match the parameters passed in, and then re-started. Settings whose parameters are omitted are reset to their default values. By default, the service is installed to run as `NetworkService`. Use the `Credential` parameter to run as a different account. This user will be granted the logon as a service right. To run as a system account other than `NetworkService`, provide just the account's name as the `UserName` parameter. [Managed service accounts and virtual accounts](http://technet.microsoft.com/en-us/library/dd548356.aspx) should be supported (we don't know how to test, so can't be sure). Pass their name to the `UserName` parameter. The minimum required information to install a service is its name and path. Manual services are not started. Automatic services are started after installation. If an existing manual service is running when configuration begins, it is re-started after re-configured. If a service is stopped when configuration begins, it remains stopped when configuration ends. To start the service if it is stopped, use the `-EnsureRunning` switch. .LINK Uninstall-CService .LINK http://technet.microsoft.com/en-us/library/dd548356.aspx .EXAMPLE Install-CService -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe Installs the Death Star service, which runs the service executable at `C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe`. The service runs as `NetworkService` and will start automatically. .EXAMPLE Install-CService -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -StartupType Manual Install the Death Star service to startup manually. You certainly don't want the thing roaming the galaxy, destroying things willy-nilly, do you? .EXAMPLE Install-CService -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -StartupType Automatic -Delayed Demonstrates how to set a service startup typemode to automatic delayed. Set the `StartupType` parameter to `Automatic` and provide the `Delayed` switch. This behavior was added in Carbon 2.5. .EXAMPLE Install-CService -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -Credential $tarkin Installs the Death Star service to run as Grand Moff Tarkin, who is also given the log on as a service right. .EXAMPLE Install-CService -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -Username SYSTEM Demonstrates how to install a service to run as a system account, gMSA, or virtual account other than `NetworkService`. In this example, installs the DeathStar service to run as the local `System` account. .EXAMPLE Install-CService -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -OnFirstFailure RunCommand -RunCommandDelay '0:00:05' -Command 'engage_hyperdrive.exe "Corruscant"' -OnSecondFailure Restart -RestartDelay '00:00:30' -OnThirdFailure Reboot -RebootDelay '00:02:00' -FailureResetPeriod '1.00:00:00' Demonstrates how to control the service's failure actions. On the first failure, Windows will run the `engage-hyperdrive.exe "Corruscant"` command after 5 seconds. On the second failure, Windows will restart the service after 30 seconds. On the third failure, Windows will reboot after two minutes. The failure count gets reset once a day. .EXAMPLE Install-CService -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -EnsureRunning Demonstrates how to ensure a service gets started after installation/configuration. Normally, `Install-CService` leaves the service in whatever state the service was in. The `EnsureRunnnig` switch will attempt to start the service even if it was stopped to begin with. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='NetworkServiceAccount')] [OutputType([ServiceProcess.ServiceController])] param( # The name of the service. [Parameter(Mandatory)] [String] $Name, # The path to the service. [Parameter(Mandatory)] [String] $Path, # The arguments/startup parameters for the service. Added in Carbon 2.0. [String[]] $ArgumentList, # The startup type: automatic, manual, or disabled. Default is automatic. # # To start the service as automatic delayed, use the `-Delayed` switch and set this parameter to `Automatic`. [ServiceStartMode] $StartupType = [ServiceStartMode]::Automatic, # When the startup type is automatic, further configure the service start type to be automatic delayed. This # parameter is ignored unless `StartupType` is `Automatic`. [switch] $Delayed, # What to do on the service's first failure. Default is to take no action. [Carbon_Windows_Service_FailureAction] $OnFirstFailure = [Carbon_Windows_Service_FailureAction]::None, # What to do on the service's second failure. Default is to take no action. [Carbon_Windows_Service_FailureAction] $OnSecondFailure = [Carbon_Windows_Service_FailureAction]::None, # What to do on the service' third failure. Default is to take no action. [Carbon_Windows_Service_FailureAction] $OnThirdFailure = [Carbon_Windows_Service_FailureAction]::None, # How often should the failure count get reset to 0? Default is to not set. Rounded to the nearest second. [TimeSpan] $FailureResetPeriod = [TimeSpan]::Zero, # How long to wait before restarting a service after a failure? Default is 1 minute. [TimeSpan] $RestartDelay = [TimeSpan]::New(0, 1, 0), # How long to wait before rebooting a server after a service failure? Default is 1 minute. [TimeSpan] $RebootDelay = [TimeSpan]::New(0, 1, 0), # What other services does this service depend on? [String[]] $Dependency, # The command to run when a service fails, including path to the command and arguments. [String] $FailureCommand, # How many milliseconds to wait before running the failure command. Default is 0, or immediately. [TimeSpan] $RunCommandDelay = [TimeSpan]::Zero, # The service's description. If you don't supply a value, the service's existing description is preserved. [String] $Description, # The service's display name. If you don't supply a value, the display name will set to Name. # # The `DisplayName` parameter was added in Carbon 2.0. [String] $DisplayName, [Parameter(ParameterSetName='CustomAccount', Mandatory)] [string] # The user the service should run as. Default is `NetworkService`. $UserName, [Parameter(ParameterSetName='CustomAccountWithCredential',Mandatory)] [pscredential] # The credential of the account the service should run as. # # The `Credential` parameter was added in Carbon 2.0. $Credential, [Switch] # Update the service even if there are no changes. $Force, [Switch] # Return a `System.ServiceProcess.ServiceController` object for the configured service. $PassThru, [Switch] # Start the service after install/configuration if it is not running. This parameter was added in Carbon 2.5.0. $EnsureRunning ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState function ConvertTo-FailureActionArg($action) { if( $action -eq 'Reboot' ) { return "reboot/{0}" -f [int]$RebootDelay.TotalMilliseconds } elseif( $action -eq 'Restart' ) { return "restart/{0}" -f [int]$RestartDelay.TotalMilliseconds } elseif( $action -eq 'RunCommand' ) { return 'run/{0}' -f [int]$RunCommandDelay.TotalMilliseconds } elseif( $action -eq 'None' ) { return '""/0' } else { Write-Error "Service failure action '$action' not found/recognized." return '' } } function Select-FailureAction { param( [Parameter(Mandatory, ValueFromPipeline)] [Object] $Action, [Parameter(Mandatory)] [int] $AtIndex ) begin { $idx = 0 $gotOne = $false } process { if ($idx++ -ne $AtIndex) { return } $gotOne = $true if ($null -eq $Action) { return [Carbon_Windows_Service_FailureAction]::None } return [Carbon_Windows_Service_FailureAction]$Action.Type } end { if (-not $gotOne) { return [Carbon_Windows_Service_FailureAction]::None } } } function Write-Change { param( [Parameter(Mandatory)] [String] $Property, [Parameter(Mandatory)] [AllowNull()] [AllowEmptyString()] [String] $OldValue, [Parameter(Mandatory)] [AllowNull()] [AllowEmptyString()] [String] $NewValue ) Write-Verbose "[${Name}] $('{0,-19}' -f $Property)${OldValue} -> ${NewValue}" } function ConvertTo-ArgValue { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowNull()] [AllowEmptyString()] [String] $InputObject ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState # How does PowerShell handle variables set to an empy string when part of a command? if ($null -eq $script:quotesEmptyStringArgs) { $emptyStringArg = '' $output = & (Join-Path -Path $script:moduleDirPath -ChildPath 'bin\args.exe') $emptyStringArg $script:quotesEmptyStringArgs = ($output | Measure-Object).Count -eq 1 Write-Debug "quotesEmptyStringArgs ${script:quotesEmptyStringArgs}" } } process { if ($script:quotesEmptyStringArgs -or $null -eq $InputObject) { return $InputObject } if ($InputObject -eq '') { return '""' } return $InputObject -replace '"', '\"' } } if( $PSCmdlet.ParameterSetName -like 'CustomAccount*' ) { if( $PSCmdlet.ParameterSetName -like '*WithCredential' ) { $UserName = $Credential.UserName } else { $Credential = $null } $identity = Resolve-CPrincipal -Name $UserName if( -not $identity ) { Write-Error ("Identity '{0}' not found." -f $UserName) return } } else { $identity = Resolve-CPrincipal "NetworkService" } if( -not (Test-Path -Path $Path -PathType Leaf) ) { Write-Warning ('Service ''{0}'' executable ''{1}'' not found.' -f $Name,$Path) } else { $Path = Resolve-Path -Path $Path | Select-Object -ExpandProperty ProviderPath } if( $ArgumentList ) { $binPathArg = Invoke-Command -ScriptBlock { $Path $ArgumentList } | ForEach-Object { if( $_.Contains(' ') ) { return '"{0}"' -f $_.Trim('"') } return $_ } $binPathArg = $binPathArg -join ' ' } else { $binPathArg = $Path } $passwordArgMsg = '' $passwordArgName = $null $passwordArgValue = $null if( $PSCmdlet.ParameterSetName -like 'CustomAccount*' ) { if( $Credential ) { $passwordArgName = 'password=' $password = $Credential.GetNetworkCredential().Password $passwordArgValue = $password | ConvertTo-ArgValue $passwordArgMsg = " ${passwordArgName} $('*' * $password.Length)" } if( $PSCmdlet.ShouldProcess( $identity.FullName, "grant the log on as a service right" ) ) { Grant-CPrivilege -Identity $identity.FullName -Privilege SeServiceLogonRight } } if( $PSCmdlet.ShouldProcess( $Path, ('grant {0} ReadAndExecute permissions' -f $identity.FullName) ) ) { Grant-CNtfsPermission -Identity $identity.FullName -Permission ReadAndExecute -Path $Path } $doInstall = $doFailureActions = $doDescription = $false if ($Force -or -not (Test-CService -Name $Name)) { $doInstall = $doFailureActions = $doDescription = $true } else { Write-Debug -Message ('Service {0} exists. Checking if configuration has changed.' -f $Name) $service = Get-Service -Name $Name $serviceConfig = Get-CServiceConfiguration -Name $Name $dependedOnServiceNames = $service.ServicesDependedOn | Where-Object 'Name' -NE $Name | Select-Object -ExpandProperty 'Name' if( $serviceConfig.Path -ne $binPathArg ) { Write-Change 'Path' -OldValue $serviceConfig.Path -NewValue $binPathArg $doInstall = $true } # DisplayName, if not set, defaults to the service name. This makes it a little bit tricky to update. # If provided, make sure display name matches. # If not provided, reset it to an empty/default value. if ($PSBoundParameters.ContainsKey('DisplayName')) { if( $service.DisplayName -ne $DisplayName ) { Write-Change 'DisplayName' -OldValue $service.DisplayName -NewValue $DisplayName $doInstall = $true } } elseif ($service.DisplayName -ne $service.Name) { Write-Change 'DisplayName' -OldValue $service.DisplayName -NewValue '' $doInstall = $true } $firstFailure = $serviceConfig.FailureActions | Select-FailureAction -AtIndex 0 if ($firstFailure -ne $OnFirstFailure) { Write-Change 'OnFirstFailure' -OldValue $firstFailure -NewValue $OnFirstFailure $doFailureActions = $true } $secondFailure = $serviceConfig.FailureActions | Select-FailureAction -AtIndex 1 if ($secondFailure -ne $OnSecondFailure) { Write-Change 'OnSecondFailure' -OldValue $secondFailure -NewValue $OnSecondFailure $doFailureActions = $true } $thirdFailure = $serviceConfig.FailureActions | Select-FailureAction -AtIndex 2 if ($thirdFailure -ne $OnThirdFailure) { Write-Change 'OnThirdFailure' -OldValue $thirdFailure -NewValue $OnThirdFailure $doFailureActions = $true } # Failure reset period is in seconds, so make sure TimeSpan is in seconds for change detection. $FailureResetPeriod = [TimeSpan]::New(0, 0, $FailureResetPeriod.TotalSeconds) if( $serviceConfig.FailureResetPeriod -ne $FailureResetPeriod ) { Write-Change 'FailureResetPeriod' -OldValue $serviceConfig.FailureResetPeriod -NewValue $FailureResetPeriod $doFailureActions = $true } foreach ($actionType in @('Reboot', 'Restart', 'RunCommand')) { # RebootDelay, RestartDelay, and RunCommandDelay $varName = "${actionType}Delay" $expectedDelay = Get-Variable -Name $varName -ValueOnly $actions = $serviceConfig.FailureActions | Where-Object 'Type' -EQ $actionType | Where-Object 'Delay' -NE $expectedDelay if ($actions) { foreach ($action in $actions) { Write-Change $varName -OldValue $action.Delay -NewValue $expectedDelay } $doFailureActions = $true } } # Cast $null to an empty string. if ([String]$serviceConfig.FailureCommand -ne $FailureCommand) { Write-Change 'FailureCommand' -OldValue $serviceConfig.FailureCommand -NewValue $FailureCommand $doFailureActions = $true } if( $service.StartType -ne $StartupType ) { Write-Change 'StartupType' -OldValue $service.StartType -NewValue $StartupType $doInstall = $true } if( $StartupType -eq [ServiceProcess.ServiceStartMode]::Automatic -and $Delayed -ne $serviceConfig.DelayedAutoStart ) { Write-Change 'DelayedAutoStart' -OldValue $serviceConfig.DelayedAutoStart -NewValue $Delayed $doInstall = $true } if( ($Dependency | Where-Object { $dependedOnServiceNames -notcontains $_ }) -or ` ($dependedOnServiceNames | Where-Object { $Dependency -notcontains $_ }) ) { Write-Change 'Dependency' -OldValue ($dependedOnServiceNames -join ',') -NewValue ($Dependency -join ',') $doInstall = $true } if( $Description -and $serviceConfig.Description -ne $Description ) { Write-Change 'Description' -OldValue $serviceConfig.Description -NewValue $Description $doDescription = $true } $currentIdentity = Resolve-CPrincipal $serviceConfig.UserName if( $currentIdentity.FullName -ne $identity.FullName ) { Write-Change 'UserName' -OldValue $currentIdentity.FullName -NewValue $identity.FullName $doinstall = $true } } try { if (-not $doInstall -and -not $doFailureActions -and -not $doDescription) { Write-Debug -Message ('Skipping {0} service configuration: settings unchanged.' -f $Name) return } $sc = Join-Path -Path ([Environment]::GetFolderPath('System')) -ChildPath 'sc.exe' -Resolve if( $Dependency ) { $missingDependencies = $false foreach ($dependencyName in $Dependency) { if (-not (Test-CService -Name $dependencyName)) { $msg = "Dependent service ""${dependencyName}"" not found." Write-Error -Message $msg -ErrorAction $ErrorActionPreference $missingDependencies = $true } } if( $missingDependencies ) { return } } $startArg = 'auto' if ($StartupType -eq [ServiceStartMode]::Automatic -and $Delayed) { $startArg = 'delayed-auto' } elseif ($StartupType -eq [ServiceStartMode]::Manual) { $startArg = 'demand' } elseif ($StartupType -eq [ServiceStartMode]::Disabled) { $startArg = 'disabled' } $service = Get-Service -Name $Name -ErrorAction Ignore $operation = 'create' $serviceIsRunningStatus = [ServiceControllerStatus]::Running, [ServiceControllerStatus]::StartPending if( -not $EnsureRunning ) { $EnsureRunning = ($StartupType -eq [ServiceStartMode]::Automatic) } if( $service ) { $EnsureRunning = ( $EnsureRunning -or ($serviceIsRunningStatus -contains $service.Status) ) if ($StartupType -eq [ServiceStartMode]::Disabled) { $EnsureRunning = $false } if( $service.CanStop ) { Stop-Service -Name $Name -Force -ErrorAction Ignore if( $? ) { $service.WaitForStatus( 'Stopped' ) } } if (-not ($service.Status -eq [ServiceControllerStatus]::Stopped)) { $msg = "Unable to stop service ""${Name}"" before applying configuration changes. You may need to " + 'restart this service manually for any changes to take affect.' Write-Warning $msg -WarningAction $WarningPreference } $operation = 'config' } $dependencyArgMsg = '' $dependencyArgName = $null $dependencyArgValue = $null if ($Dependency -or $doInstall) { $dependencyArgName = 'depend=' $dependencyArgValue = $Dependency -join '/' | ConvertTo-ArgValue $dependencyArgMsg = " ${dependencyArgName} ${dependencyArgValue}" } $displayNameArgMsg = '' $displayNameArgName = $null $displayNameArgValue = $null if ($DisplayName -or $doInstall) { $displayNameArgName ='DisplayName=' $displayNameArgValue = $DisplayName | ConvertTo-ArgValue $displayNameArgMsg = " ${displayNameArgName} ${displayNameArgValue}" } $target = "service ""${Name}""" $binPathArg = $binPathArg | ConvertTo-ArgValue if ($doInstall -and $PSCmdlet.ShouldProcess($target, $operation)) { $msg = "${sc} ${operation} ${Name} binPath= ${binPathArg} start= ${startArg} obj= $($identity.FullName)" + "${passwordArgMsg}${dependencyArgMsg}${displayNameArgMsg}" Write-Information $msg & $sc $operation ` $Name ` binPath= $binPathArg ` start= $startArg ` obj= $identity.FullName ` $passwordArgName $passwordArgValue ` $dependencyArgName $dependencyArgValue ` $displayNameArgName $displayNameArgValue | Write-Verbose $scExitCode = $LASTEXITCODE if( $scExitCode -ne 0 ) { $reason = net helpmsg $scExitCode 2>$null | Where-Object { $_ } if ($scExitCode -eq 1078) { & $sc queryex $Name | Write-Verbose -Verbose & $sc qc $Name | Write-Verbose -Verbose } Write-Error ("Failed to {0} service '{1}'. {2} returned exit code {3}: {4}" -f $operation,$Name,$sc,$scExitCode,$reason) return } } if ($doDescription -and $PSCmdlet.ShouldProcess($target, 'set description')) { Write-Information "${sc} description ${Name} ${Description}" & $sc 'description' $Name ($Description | ConvertTo-ArgValue) | Write-Verbose $scExitCode = $LASTEXITCODE if( $scExitCode -ne 0 ) { $reason = net helpmsg $scExitCode 2>$null | Where-Object { $_ } Write-Error ("Failed to set {0} service's description. {1} returned exit code {2}: {3}" -f $Name,$sc,$scExitCode,$reason) return } } $firstAction = ConvertTo-FailureActionArg $OnFirstFailure $secondAction = ConvertTo-FailureActionArg $OnSecondFailure $thirdAction = ConvertTo-FailureActionArg $OnThirdFailure $failureCommandArgValue = $FailureCommand | ConvertTo-ArgValue if ($doFailureActions -and $PSCmdlet.ShouldProcess($target, 'set failure actions')) { $failureResetPeriodSeconds = [int]$FailureResetPeriod.TotalSeconds $msg = "${sc} failure ${Name} reset= ${failureResetPeriodSeconds} " + "${firstAction}/${secondAction}/${thirdAction} command= ${failureCommandArgValue}" Write-Information $msg & $sc failure ` $Name ` reset= $failureResetPeriodSeconds ` actions= $firstAction/$secondAction/$thirdAction ` command= $failureCommandArgValue | Write-Verbose $scExitCode = $LASTEXITCODE if( $scExitCode -ne 0 ) { $reason = net helpmsg $scExitCode 2>$null | Where-Object { $_ } Write-Error ("Failed to set {0} service's failure actions. {1} returned exit code {2}: {3}" -f $Name,$sc,$scExitCode,$reason) return } } } finally { if( $EnsureRunning ) { if( $PSCmdlet.ShouldProcess( $Name, 'start service' ) ) { Start-Service -Name $Name -ErrorAction $ErrorActionPreference -WarningAction SilentlyContinue if( (Get-Service -Name $Name).Status -ne [ServiceControllerStatus]::Running ) { if( $PSCmdlet.ParameterSetName -like 'CustomAccount*' -and -not $Credential ) { Write-Warning ('Service ''{0}'' didn''t start and you didn''t supply a password to Install-CService. Is ''{1}'' a managed service account or virtual account? (See http://technet.microsoft.com/en-us/library/dd548356.aspx.) If not, please use the `Credential` parameter to pass the account''s credentials.' -f $Name,$UserName) } else { Write-Warning ('Failed to re-start service ''{0}''.' -f $Name) } } } } else { Write-Verbose ('Not re-starting {0} service. Its startup type is {1} and it wasn''t running when configuration began. To always start a service after configuring it, use the -EnsureRunning switch.' -f $Name,$StartupType) } if( $PassThru ) { Get-Service -Name $Name -ErrorAction Ignore } } } function Restart-CRemoteService { <# .SYNOPSIS Restarts a service on a remote machine. .DESCRIPTION One of the annoying features of PowerShell is that the `Stop-Service`, `Start-Service` and `Restart-Service` cmdlets don't have `ComputerName` parameters to start/stop/restart a service on a remote computer. You have to use `Get-Service` to get the remote service: $service = Get-Service -Name DeathStar -ComputerName Yavin $service.Stop() $service.Start() # or (and no, you can't pipe the service directly to `Restart-Service`) Get-Service -Name DeathStar -ComputerName Yavin | ForEach-Object { Restart-Service -InputObject $_ } This function does all this unnecessary work for you. You'll get an error if you attempt to restart a non-existent service. .EXAMPLE Restart-CRemoteService -Name DeathStar -ComputerName Yavin Restarts the `DeathStar` service on Yavin. If the DeathStar service doesn't exist, you'll get an error. #> [CmdletBinding(SupportsShouldProcess=$true)] param( [Parameter(Mandatory=$true)] [string] # The service name to restart. $Name, [Parameter(Mandatory=$true)] [string] # The name of the computer where the service lives. $ComputerName ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $service = Get-Service -Name $name -ComputerName $computerName if($service) { if($pscmdlet.ShouldProcess( "$name on $computerName", "restart")) { $service.Stop() $service.Start() } } else { Write-Error "Unable to restart remote service because I could not get a reference to service $name on machine: $computerName" } } function Revoke-CServicePermission { <# .SYNOPSIS Removes permissions to a service. .DESCRIPTION The `Revoke-CServicePermission` function removes a principal's permissions to a Windows service. Pass the service's name to the `Name` parameter, and the principal's name whose permissions to remove to the `PrincipalName` parameter. If the user has permissions, they are removed. If the user has no permissions, nothing happens. .LINK Get-CServicePermission .LINK Grant-CServicePermission .EXAMPLE Revoke-CServicePermission -Name 'Hyperdrive` -PrincipalName 'CLOUDCITY\LCalrissian' Removes all of Lando's permissions to control the `Hyperdrive` service. #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')] param( # The service. [Parameter(Mandatory)] [String] $Name, # The principal whose permissions to remove. [Parameter(Mandatory)] [String] $PrincipalName ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $account = Resolve-CPrincipal -Name $PrincipalName if( -not $account ) { return } if( -not (Assert-CService -Name $Name) ) { return } if( (Get-CServicePermission -Name $Name -PrincipalName $account.FullName) ) { $msg = "[Revoke-CServicePermission] Removing ""$($account.FullName)"" principal's permissions to the " + """${Name}"" service." Write-Information $msg $dacl = Get-CServiceAcl -Name $Name $dacl.Purge( $account.Sid ) Set-CServiceAcl -Name $Name -Dacl $dacl } } function Set-CServiceAcl { <# .SYNOPSIS Sets a service's discretionary access control list (i.e. DACL). .DESCRIPTION The existing DACL is replaced with the new DACL. No previous permissions are preserved. That's your job. You're warned! You probably want `Grant-CServicePermission` or `Revoke-CServicePermission` instead. .LINK Get-CServicePermission .LINK Grant-CServicePermission .LINK Revoke-CServicePermission .EXAMPLE Set-ServiceDacl -Name 'Hyperdrive' -Dacl $dacl Replaces the DACL on the `Hyperdrive` service. Yikes! Sounds like something the Empire would do, though. #> [CmdletBinding(SupportsShouldProcess)] param( # The service whose DACL to replace. [Parameter(Mandatory)] [String] $Name, # The service's new DACL. [Parameter(Mandatory)] [DiscretionaryAcl] $Dacl ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $daclBytes = [byte[]]::New($Dacl.BinaryLength) $Dacl.GetBinaryForm($daclBytes, 0) $rawAcl = [RawAcl]::New($daclBytes, 0) $sd = [RawSecurityDescriptor]::New([ControlFlags]::DiscretionaryAclPresent, $null, $null, $null, $rawAcl) $scmHandle = Invoke-AdvApiOpenSCManager if (-not $scmHandle) { return } try { $svcHandle = Invoke-AdvApiOpenService -SCManagerHandle $scmHandle -ServiceName $Name -DesiredAccess WriteDac if (-not $svcHandle) { return } try { if( $PSCmdlet.ShouldProcess( ("{0} service DACL" -f $Name), "set" ) ) { Invoke-AdvApiSetServiceObjectSecurity -ServiceHandle $svcHandle ` -SecurityInformation DiscretionaryAcl ` -SecurityDescriptor $sd } } finally { $svcHandle | Invoke-AdvApiCloseServiceHandle } } finally { $scmHandle | Invoke-AdvApiCloseServiceHandle } } function Test-CService { <# .SYNOPSIS Tests if a service exists, without writing anything out to the error stream. .DESCRIPTION `Get-Service` writes an error when a service doesn't exist. This function tests if a service exists without writing anyting to the output stream. .OUTPUTS System.Boolean. .LINK Carbon_Service .LINK Install-CService .LINK Uninstall-CService .EXAMPLE Test-CService -Name 'Drive' Returns `true` if the `Drive` service exists. `False` otherwise. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] # The name of the service to test. $Name ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $service = Get-Service -Name $Name -ErrorAction Ignore if( $service ) { return $true } else { return $false } } function Uninstall-CService { <# .SYNOPSIS Removes/deletes a Windows service. .DESCRIPTION The `Uninstall-CService` function removes an existing Windows service. If the service doesn't exist, nothing happens. The service is stopped before being deleted, so that the computer doesn't need to be restarted for the removal to complete. If the service's process is still running after the service is stopped (some services don't behave nicely) and the service is only running one process, `Uninstall-CService` will kill the service's process. This helps prevent requiring a reboot. Use the `StopTimeout` parameter to give the service time to stop before killing its process. The default is to kill the process immediately after `Stop-Process` returns. .LINK Install-CService .EXAMPLE Uninstall-CService -Name DeathStar Removes the Death Star Windows service. It is destro..., er, stopped first, then destro..., er, deleted. If only the rebels weren't using Linux! #> [CmdletBinding(SupportsShouldProcess)] param( # The service name to delete. [Parameter(Mandatory)] [String] $Name, # The amount of time to wait for the service to stop before attempting to kill it. The default is not to wait. [TimeSpan] $StopTimeout ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $service = Get-Service -ErrorAction Ignore | Where-Object 'Name' -EQ $Name if( -not $service ) { return } $origVerbosePref = $VerbosePreference $VerbosePreference = 'SilentlyContinue' $cimService = Get-CimInstance 'Win32_Service' -Filter ('Name = ''{0}''' -f $service.Name) $cimServiceProcessCount = 0 if( $cimService ) { $cimServiceProcessCount = Get-CimInstance 'Win32_Service' -Filter ('ProcessId = ''{0}''' -f $cimService.ProcessId) | Measure-Object | Select-Object -ExpandProperty 'Count' } $VerbosePreference = $origVerbosePref if( -not $pscmdlet.ShouldProcess( "service '$Name'", "remove" ) ) { return } $nameMsg = """$($service.DisplayName)"" (${Name})" if ($service.DisplayName -eq $Name) { $nameMsg = """${Name}""" } Write-Information "Uninstalling ${nameMsg} service." Stop-Service $Name if( $cimService -and $cimServiceProcessCount -eq 1 ) { $process = Get-Process -Id $cimService.ProcessId -ErrorAction Ignore if( $process ) { $killService = $true if( $StopTimeout ) { Write-Verbose -Message ('[Uninstall-CService] [{0}] Waiting "{1}" second(s) for service process "{2}" to exit.' -f $Name,$StopTimeout.TotalSeconds,$process.Id) $killService = -not $process.WaitForExit($StopTimeout.TotalMilliseconds) } if( $killService ) { $attemptNum = 0 $maxAttempts = 100 $killed = $false while( $attemptNum++ -lt $maxAttempts ) { Write-Verbose -Message ('[Uninstall-CService] [{0}] [Attempt {1,3} of {2}] Killing service process "{3}".' -f $Name,$attemptNum,$maxAttempts,$process.Id) Stop-Process -Id $process.Id -Force -ErrorAction Ignore if( -not (Get-Process -Id $process.Id -ErrorAction Ignore) ) { $killed = $true break } Start-Sleep -Milliseconds 100 } if( -not $killed ) { Write-Error -Message ('Failed to kill "{0}" service process "{1}".' -f $Name,$process.Id) -ErrorAction $ErrorActionPreference } } } } $sc = Join-Path -Path ([Environment]::GetFolderPath('System')) -ChildPath 'sc.exe' -Resolve Write-Verbose -Message ('[Uninstall-CService] [{0}] {1} delete {0}' -f $Name,$sc) $output = & $sc delete $Name if( $LASTEXITCODE ) { if( $LASTEXITCODE -eq 1072 ) { Write-Warning -Message ('The {0} service is marked for deletion and will be removed during the next reboot.{1}{2}' -f $Name,([Environment]::NewLine),($output -join ([Environment]::NewLine))) } else { Write-Error -Message ('Failed to uninstall {0} service (returned non-zero exit code {1}):{2}{3}' -f $Name,$LASTEXITCODE,([Environment]::NewLine),($output -join ([Environment]::NewLine))) } } else { $output | Write-Verbose } } function Use-CallerPreference { <# .SYNOPSIS Sets the PowerShell preference variables in a module's function based on the callers preferences. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module. When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller: * ErrorAction * Debug * Confirm * InformationAction * Verbose * WarningAction * WhatIf This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function. This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d). There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get fixed. .LINK about_Preference_Variables .LINK about_CommonParameters .LINK https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d .LINK http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/ .EXAMPLE Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Demonstrates how to set the caller's common parameter preference variables in a module function. #> [CmdletBinding()] param ( [Parameter(Mandatory)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` # attribute. $Cmdlet, [Parameter(Mandatory)] # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the # `[CmdletBinding()]` attribute. # # Used to set variables in its callers' scope, even if that caller is in a different script module. [Management.Automation.SessionState]$SessionState ) Set-StrictMode -Version 'Latest' # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken # from about_CommonParameters). $commonPreferences = @{ 'ErrorActionPreference' = 'ErrorAction'; 'DebugPreference' = 'Debug'; 'ConfirmPreference' = 'Confirm'; 'InformationPreference' = 'InformationAction'; 'VerbosePreference' = 'Verbose'; 'WarningPreference' = 'WarningAction'; 'WhatIfPreference' = 'WhatIf'; } foreach( $prefName in $commonPreferences.Keys ) { $parameterName = $commonPreferences[$prefName] # Don't do anything if the parameter was passed in. if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) { continue } $variable = $Cmdlet.SessionState.PSVariable.Get($prefName) # Don't do anything if caller didn't use a common parameter. if( -not $variable ) { continue } if( $SessionState -eq $ExecutionContext.SessionState ) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } |