Functions/Install-Service.ps1
# 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. function Install-Service { <# .SYNOPSIS Installs a Windows service. .DESCRIPTION `Install-Service` 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. Beginning in Carbon 2.0, use the `PassThru` switch to return a `ServiceController` object for the new/updated service. By default, the service is installed to run as `NetworkService`. Use the `Credential` parameter to run as a different account (if you don't have a `Credential` parameter, upgrade to Carbon 2.0 or use the `UserName` and `Password` parameters). 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. The minimum required information to install a service is its name and path. [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). Simply omit the `-Password` parameter when providing a custom account name with the `-Username` parameter. `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. The ability to provide service arguments/parameters via the `ArgumentList` parameter was added in Carbon 2.0. .LINK Carbon_Service .LINK New-Credential .LINK Uninstall-Service .LINK http://technet.microsoft.com/en-us/library/dd548356.aspx .EXAMPLE Install-Service -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-Service -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 thing willy-nilly, do you? .EXAMPLE Install-Service -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -Credential $tarkinCredentials Installs the Death Star service to run as Grand Moff Tarkin, who is given the log on as a service right. .EXAMPLE Install-Service -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -Username SYSTEM Demonstrates how to install a service to run as a system account other than `NetworkService`. Installs the DeathStart service to run as the local `System` account. .EXAMPLE Install-Service -Name DeathStar -Path C:\ALongTimeAgo\InAGalaxyFarFarAway\DeathStar.exe -OnFirstFailure RunCommand -RunCommandDelay 5000 -Command 'engage_hyperdrive.exe "Corruscant"' -OnSecondFailure Restart -RestartDelay 30000 -OnThirdFailure Reboot -RebootDelay 120000 -ResetFailureCount (60*60*24) 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 (`5,000` milliseconds). On the second failure, Windows will restart the service after 30 seconds (`30,000` milliseconds). On the third failure, Windows will reboot after two minutes (`120,000` milliseconds). The failure count gets reset once a day (`60*60*24` seconds). #> [CmdletBinding(SupportsShouldProcess=$true,DefaultParameterSetName='NetworkServiceAccount')] [OutputType([ServiceProcess.ServiceController])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams","")] param( [Parameter(Mandatory=$true)] [string] # The name of the service. $Name, [Parameter(Mandatory=$true)] [string] # The path to the service. $Path, [string[]] # The arguments/startup parameters for the service. Added in Carbon 2.0. $ArgumentList, [ServiceProcess.ServiceStartMode] # The startup type: automatic, manual, or disabled. Default is automatic. $StartupType = [ServiceProcess.ServiceStartMode]::Automatic, [Carbon.Service.FailureAction] # What to do on the service's first failure. Default is to take no action. $OnFirstFailure = [Carbon.Service.FailureAction]::TakeNoAction, [Carbon.Service.FailureAction] # What to do on the service's second failure. Default is to take no action. $OnSecondFailure = [Carbon.Service.FailureAction]::TakeNoAction, [Carbon.Service.FailureAction] # What to do on the service' third failure. Default is to take no action. $OnThirdFailure = [Carbon.Service.FailureAction]::TakeNoAction, [int] # How many seconds after which the failure count is reset to 0. $ResetFailureCount = 0, [int] # How many milliseconds to wait before restarting the service. Default is 60,0000, or 1 minute. $RestartDelay = 60000, [int] # How many milliseconds to wait before handling the second failure. Default is 60,000 or 1 minute. $RebootDelay = 60000, [Alias('Dependencies')] [string[]] # What other services does this service depend on? $Dependency, [string] # The command to run when a service fails, including path to the command and arguments. $Command, [int] # How many milliseconds to wait before running the failure command. Default is 0, or immediately. $RunCommandDelay = 0, [string] # The service's description. If you don't supply a value, the service's existing description is preserved. # # The `Description` parameter was added in Carbon 2.0. $Description, [string] # 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. $DisplayName, [Parameter(ParameterSetName='CustomAccount',Mandatory=$true)] [string] # The user the service should run as. Default is `NetworkService`. $UserName, [Parameter(ParameterSetName='CustomAccount',DontShow=$true)] [string] # OBSOLETE. The `Password` parameter will be removed in a future major version of Carbon. Use the `Credential` parameter instead. $Password, [Parameter(ParameterSetName='CustomAccountWithCredential',Mandatory=$true)] [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 ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState function ConvertTo-FailureActionArg($action) { if( $action -eq 'Reboot' ) { return "reboot/{0}" -f $RebootDelay } elseif( $action -eq 'Restart' ) { return "restart/{0}" -f $RestartDelay } elseif( $action -eq 'RunCommand' ) { return 'run/{0}' -f $RunCommandDelay } elseif( $action -eq 'TakeNoAction' ) { return '""/0' } else { Write-Error "Service failure action '$action' not found/recognized." return '' } } if( $PSCmdlet.ParameterSetName -like 'CustomAccount*' ) { if( $PSCmdlet.ParameterSetName -like '*WithCredential' ) { $UserName = $Credential.UserName } elseif( $Password ) { Write-Warning ('`Install-Service` function''s `Password` parameter is obsolete and will be removed in a future major version of Carbon. Please use the `Credential` parameter instead.') $Credential = New-Credential -UserName $UserName -Password $Password } else { $Credential = $null } $identity = Resolve-Identity -Name $UserName if( -not $identity ) { Write-Error ("Identity '{0}' not found." -f $UserName) return } } else { $identity = Resolve-Identity "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 } $doInstall = $false if( -not $Force -and (Test-Service -Name $Name) ) { Write-Debug -Message ('Service {0} exists. Checking if configuration has changed.' -f $Name) $service = Get-Service -Name $Name $serviceConfig = Get-ServiceConfiguration -Name $Name $dependedOnServiceNames = $service.ServicesDependedOn | Select-Object -ExpandProperty 'Name' if( $service.Path -ne $binPathArg ) { Write-Verbose ('[{0}] Path {1} -> {2}' -f $Name,$serviceConfig.Path,$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-Verbose ('[{0}] DisplayName {1} -> {2}' -f $Name,$service.DisplayName,$DisplayName) $doInstall = $true } } elseif( $service.DisplayName -ne $service.Name ) { Write-Verbose ('[{0}] DisplayName {1} -> ' -f $Name,$service.DisplayName) $doInstall = $true } if( $serviceConfig.FirstFailure -ne $OnFirstFailure ) { Write-Verbose ('[{0}] OnFirstFailure {1} -> {2}' -f $Name,$serviceConfig.FirstFailure,$OnFirstFailure) $doInstall = $true } if( $serviceConfig.SecondFailure -ne $OnSecondFailure ) { Write-Verbose ('[{0}] OnSecondFailure {1} -> {2}' -f $Name,$serviceConfig.SecondFailure,$OnSecondFailure) $doInstall = $true } if( $serviceConfig.ThirdFailure -ne $OnThirdFailure ) { Write-Verbose ('[{0}] OnThirdFailure {1} -> {2}' -f $Name,$serviceConfig.ThirdFailure,$OnThirdFailure) $doInstall = $true } if( $serviceConfig.ResetPeriod -ne $ResetFailureCount ) { Write-Verbose ('[{0}] ResetFailureCount {1} -> {2}' -f $Name,$serviceConfig.ResetPeriod,$ResetFailureCount) $doInstall = $true } $failureActions = $OnFirstFailure,$OnSecondFailure,$OnThirdFailure if( ($failureActions | Where-Object { $_ -eq [Carbon.Service.FailureAction]::Reboot }) -and $serviceConfig.RebootDelay -ne $RebootDelay ) { Write-Verbose ('[{0}] RebootDelay {1} -> {2}' -f $Name,$serviceConfig.RebootDelay,$RebootDelay) $doInstall = $true } if( ($failureActions | Where-Object { $_ -eq [Carbon.Service.FailureAction]::Restart }) -and $serviceConfig.RestartDelay -ne $RestartDelay) { Write-Verbose ('[{0}] RestartDelay {1} -> {2}' -f $Name,$serviceConfig.RestartDelay,$RestartDelay) $doInstall = $true } if( $failureActions | Where-Object { $_ -eq [Carbon.Service.FailureAction]::RunCommand } ) { if( $serviceConfig.FailureProgram -ne $Command ) { Write-Verbose ('[{0}] Command {1} -> {2}' -f $Name,$serviceConfig.FailureProgram,$Command) $doInstall = $true } if( $serviceConfig.RunCommandDelay -ne $RunCommandDelay ) { Write-Verbose ('[{0}] RunCommandDelay {1} -> {2}' -f $Name,$serviceConfig.RunCommandDelay,$RunCommandDelay) $doInstall = $true } } if( $service.StartMode -ne $StartupType ) { Write-Verbose ('[{0}] StartupType {1} -> {2}' -f $Name,$serviceConfig.StartType,$StartupType) $doInstall = $true } if( ($Dependency | Where-Object { $dependedOnServiceNames -notcontains $_ }) -or ` ($dependedOnServiceNames | Where-Object { $Dependency -notcontains $_ }) ) { Write-Verbose ('[{0}] Dependency {1} -> {2}' -f $Name,($dependedOnServiceNames -join ','),($Dependency -join ',')) $doInstall = $true } if( $Description -and $serviceConfig.Description -ne $Description ) { Write-Verbose ('[{0}] Description {1} -> {2}' -f $Name,$serviceConfig.Description,$Description) $doInstall = $true } if( $PSCmdlet.ParameterSetName -like 'CustomAccount*' -and $serviceConfig.UserName -ne $identity.FullName ) { Write-Verbose ('[{0}] UserName {1} -> {2}' -f $Name,$serviceConfig.UserName,$identity.FullName) $doinstall = $true } } else { $doInstall = $true } if( -not $doInstall ) { Write-Debug -Message ('Skipping {0} service configuration: settings unchanged.' -f $Name) if( $PassThru ) { Get-Service -Name $Name -ErrorAction Ignore } return } if( $Dependency ) { $missingDependencies = $false $Dependency | ForEach-Object { if( -not (Test-Service -Name $_) ) { Write-Error ('Dependent service {0} not found.' -f $_) $missingDependencies = $true } } if( $missingDependencies ) { return } } $sc = Join-Path $env:WinDir system32\sc.exe -Resolve $startArg = 'auto' if( $StartupType -eq 'Manual' ) { $startArg = 'demand' } elseif( $StartupType -eq 'Disabled' ) { $startArg = 'disabled' } $passwordArgName = '' $passwordArgValue = '' if( $PSCmdlet.ParameterSetName -like 'CustomAccount*' ) { if( $Credential ) { $passwordArgName = 'password=' $passwordArgValue = $Credential.GetNetworkCredential().Password -replace '"', '\"' } if( $PSCmdlet.ShouldProcess( $identity.FullName, "grant the log on as a service right" ) ) { Grant-Privilege -Identity $identity.FullName -Privilege SeServiceLogonRight } } if( $PSCmdlet.ShouldProcess( $Path, ('grant {0} ReadAndExecute permissions' -f $identity.FullName) ) ) { Grant-Permission -Identity $identity.FullName -Permission ReadAndExecute -Path $Path } $service = Get-Service -Name $Name -ErrorAction Ignore $operation = 'create' $serviceIsRunningStatus = @( [ServiceProcess.ServiceControllerStatus]::Running, [ServiceProcess.ServiceControllerStatus]::StartPending ) $restartService = ($StartupType -eq [ServiceProcess.ServiceStartMode]::Automatic) if( $service ) { $restartService = ( $restartService -or ($serviceIsRunningStatus -contains $service.Status) ) if( $service.CanStop ) { Stop-Service -Name $Name -Force -ErrorAction Ignore if( $? ) { $service.WaitForStatus( 'Stopped' ) } } if( -not ($service.Status -eq [ServiceProcess.ServiceControllerStatus]::Stopped) ) { Write-Warning "Unable to stop service '$Name' before applying config changes. You may need to restart this service manually for any changes to take affect." } $operation = 'config' } $dependencyArgValue = '""' if( $Dependency ) { $dependencyArgValue = $Dependency -join '/' } $displayNameArgName = 'DisplayName=' $displayNameArgValue = '""' if( $DisplayName ) { $displayNameArgValue = $DisplayName } $binPathArg = $binPathArg -replace '"','\"' if( $PSCmdlet.ShouldProcess( "$Name [$Path]", "$operation service" ) ) { & $sc $operation $Name binPath= $binPathArg start= $startArg obj= $identity.FullName $passwordArgName $passwordArgValue depend= $dependencyArgValue $displayNameArgName $displayNameArgValue | Write-Verbose $scExitCode = $LastExitCode if( $scExitCode -ne 0 ) { $reason = net helpmsg $scExitCode 2>$null | Where-Object { $_ } Write-Error ("Falied to {0} service '{1}'. {2} returned exit code {3}: {4}" -f $operation,$Name,$sc,$scExitCode,$reason) return } if( $Description ) { & $sc 'description' $Name $Description | Write-Verbose $scExitCode = $LastExitCode if( $scExitCode -ne 0 ) { $reason = net helpmsg $scExitCode 2>$null | Where-Object { $_ } Write-Error ("Falied 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 if( -not $Command ) { $Command = '""' } if( $PSCmdlet.ShouldProcess( $Name, "setting service failure actions" ) ) { & $sc failure $Name reset= $ResetFailureCount actions= $firstAction/$secondAction/$thirdAction command= $Command | 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 } } if( $restartService ) { if( $PSCmdlet.ShouldProcess( $Name, 'start service' ) ) { Start-Service -Name $Name -ErrorAction $ErrorActionPreference if( (Get-Service -Name $Name).Status -ne [ServiceProcess.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-Service. 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.' -f $Name,$StartupType) } if( $PassThru ) { Get-Service -Name $Name -ErrorAction Ignore } } |