mirror of
https://github.com/PowerShell/PSDscResources
synced 2026-06-21 13:45:29 +00:00
1784 lines
58 KiB
PowerShell
1784 lines
58 KiB
PowerShell
<#
|
|
Error codes and their meanings for Invoke-CimMethod on a Win32_Service can be found here:
|
|
https://msdn.microsoft.com/en-us/library/aa384901(v=vs.85).aspx
|
|
#>
|
|
|
|
$errorActionPreference = 'Stop'
|
|
Set-StrictMode -Version 'Latest'
|
|
|
|
# Import CommonResourceHelper for Get-LocalizedData, Test-IsNanoServer, New-InvalidArgumentException, New-InvalidOperationException
|
|
$script:dscResourcesFolderFilePath = Split-Path $PSScriptRoot -Parent
|
|
$script:commonResourceHelperFilePath = Join-Path -Path $script:dscResourcesFolderFilePath -ChildPath 'CommonResourceHelper.psm1'
|
|
Import-Module -Name $script:commonResourceHelperFilePath
|
|
|
|
# Localized messages for verbose and error statements in this resource
|
|
$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ServiceResource'
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Retrieves the current status of the service resource with the given name.
|
|
|
|
.PARAMETER Name
|
|
The name of the service to retrieve the status of.
|
|
|
|
This may be different from the service's display name.
|
|
To retrieve a list of all services with their names and current states, use the Get-Service
|
|
cmdlet.
|
|
#>
|
|
function Get-TargetResource
|
|
{
|
|
[OutputType([System.Collections.Hashtable])]
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Name
|
|
)
|
|
|
|
$service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue'
|
|
|
|
if ($null -ne $service)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceExists -f $Name)
|
|
|
|
$serviceCimInstance = Get-ServiceCimInstance -ServiceName $Name
|
|
|
|
$dependencies = @()
|
|
|
|
foreach ($serviceDependedOn in $service.ServicesDependedOn)
|
|
{
|
|
$dependencies += $serviceDependedOn.Name.ToString()
|
|
}
|
|
|
|
$startupType = ConvertTo-StartupTypeString -StartMode $serviceCimInstance.StartMode
|
|
|
|
$builtInAccount = switch ($serviceCimInstance.StartName)
|
|
{
|
|
'NT Authority\NetworkService' { 'NetworkService'; break }
|
|
'NT Authority\LocalService' { 'LocalService'; break }
|
|
default { $serviceCimInstance.StartName }
|
|
}
|
|
|
|
$serviceResource = @{
|
|
Name = $Name
|
|
Ensure = 'Present'
|
|
Path = $serviceCimInstance.PathName
|
|
StartupType = $startupType
|
|
BuiltInAccount = $builtInAccount
|
|
State = $service.Status.ToString()
|
|
DisplayName = $service.DisplayName
|
|
Description = $serviceCimInstance.Description
|
|
DesktopInteract = $serviceCimInstance.DesktopInteract
|
|
Dependencies = $dependencies
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceDoesNotExist -f $Name)
|
|
$serviceResource = @{
|
|
Name = $Name
|
|
Ensure = 'Absent'
|
|
}
|
|
}
|
|
|
|
return $serviceResource
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Creates, modifies, or deletes the service with the given name.
|
|
|
|
.PARAMETER Name
|
|
The name of the service to create, modify, or delete.
|
|
|
|
This may be different from the service's display name.
|
|
To retrieve a list of all services with their names and current states, use the Get-Service
|
|
cmdlet.
|
|
|
|
.PARAMETER Ensure
|
|
Specifies whether the service should exist or not.
|
|
|
|
Set this property to Present to create or modify a service.
|
|
Set this property to Absent to delete a service.
|
|
|
|
The default value is Present.
|
|
|
|
.PARAMETER Path
|
|
The path to the executable the service should run.
|
|
Required when creating a service.
|
|
|
|
The user account specified by BuiltInAccount or Credential must have access to this path in
|
|
order to start the service.
|
|
|
|
.PARAMETER StartupType
|
|
The startup type the service should have.
|
|
|
|
.PARAMETER BuiltInAccount
|
|
The built-in account the service should start under.
|
|
|
|
Cannot be specified at the same time as Credential.
|
|
|
|
The user account specified by this property must have access to the service executable path
|
|
defined by Path in order to start the service.
|
|
|
|
.PARAMETER DesktopInteract
|
|
Indicates whether or not the service should be able to communicate with a window on the
|
|
desktop.
|
|
|
|
Must be false for services not running as LocalSystem.
|
|
The default value is false.
|
|
|
|
.PARAMETER State
|
|
The state the service should be in.
|
|
The default value is Running.
|
|
|
|
To disregard the state that the service is in, specify this property as Ignore.
|
|
|
|
.PARAMETER DisplayName
|
|
The display name the service should have.
|
|
|
|
.PARAMETER Description
|
|
The description the service should have.
|
|
|
|
.PARAMETER Dependencies
|
|
An array of the names of the dependencies the service should have.
|
|
|
|
.PARAMETER StartupTimeout
|
|
The time to wait for the service to start in milliseconds.
|
|
The default value is 30000 (30 seconds).
|
|
|
|
.PARAMETER TerminateTimeout
|
|
The time to wait for the service to stop in milliseconds.
|
|
The default value is 30000 (30 seconds).
|
|
|
|
.PARAMETER Credential
|
|
The credential of the user account the service should start under.
|
|
|
|
Cannot be specified at the same time as BuiltInAccount.
|
|
The user specified by this credential will automatically be granted the Log on as a Service
|
|
right.
|
|
|
|
The user account specified by this property must have access to the service executable path
|
|
defined by Path in order to start the service.
|
|
|
|
.NOTES
|
|
SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess.
|
|
Here are the paths through which Set-TargetResource calls Invoke-CimMethod:
|
|
|
|
Set-TargetResource --> Set-ServicePath --> Invoke-CimMethod
|
|
--> Set-ServiceProperty --> Set-ServiceDependency --> Invoke-CimMethod
|
|
--> Set-ServiceAccountProperty --> Invoke-CimMethod
|
|
--> Set-ServiceStartupType --> Invoke-CimMethod
|
|
#>
|
|
function Set-TargetResource
|
|
{
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Name,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('Present', 'Absent')]
|
|
[System.String]
|
|
$Ensure = 'Present',
|
|
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Path,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('Automatic', 'Manual', 'Disabled')]
|
|
[System.String]
|
|
$StartupType,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
|
|
[System.String]
|
|
$BuiltInAccount,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('Running', 'Stopped', 'Ignore')]
|
|
[System.String]
|
|
$State = 'Running',
|
|
|
|
[Parameter()]
|
|
[System.Boolean]
|
|
$DesktopInteract = $false,
|
|
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$DisplayName,
|
|
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Description,
|
|
|
|
[Parameter()]
|
|
[System.String[]]
|
|
[AllowEmptyCollection()]
|
|
$Dependencies,
|
|
|
|
[Parameter()]
|
|
[System.UInt32]
|
|
$StartupTimeout = 30000,
|
|
|
|
[Parameter()]
|
|
[System.UInt32]
|
|
$TerminateTimeout = 30000,
|
|
|
|
[Parameter()]
|
|
[ValidateNotNull()]
|
|
[System.Management.Automation.PSCredential]
|
|
[System.Management.Automation.Credential()]
|
|
$Credential
|
|
)
|
|
|
|
if ($PSBoundParameters.ContainsKey('StartupType'))
|
|
{
|
|
Assert-NoStartupTypeStateConflict -ServiceName $Name -StartupType $StartupType -State $State
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('BuiltInAccount') -and $PSBoundParameters.ContainsKey('Credential'))
|
|
{
|
|
$errorMessage = $script:localizedData.BuiltInAccountAndCredentialSpecified -f $Name
|
|
New-InvalidArgumentException -ArgumentName 'BuiltInAccount & Credential' -Message $errorMessage
|
|
}
|
|
|
|
$service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue'
|
|
|
|
if ($Ensure -eq 'Absent')
|
|
{
|
|
if ($null -eq $service)
|
|
{
|
|
Write-Verbose -Message $script:localizedData.ServiceAlreadyAbsent
|
|
}
|
|
else
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.RemovingService -f $Name)
|
|
|
|
Stop-ServiceWithTimeout -ServiceName $Name -TerminateTimeout $TerminateTimeout
|
|
Remove-ServiceWithTimeout -Name $Name -TerminateTimeout $TerminateTimeout
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$serviceRestartNeeded = $false
|
|
|
|
# Create new service or update the service path if needed
|
|
if ($null -eq $service)
|
|
{
|
|
if ($PSBoundParameters.ContainsKey('Path'))
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.CreatingService -f $Name, $Path)
|
|
$null = New-Service -Name $Name -BinaryPathName $Path
|
|
}
|
|
else
|
|
{
|
|
$errorMessage = $script:localizedData.ServiceDoesNotExistPathMissingError -f $Name
|
|
New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ($PSBoundParameters.ContainsKey('Path'))
|
|
{
|
|
$serviceRestartNeeded = Set-ServicePath -ServiceName $Name -Path $Path
|
|
}
|
|
}
|
|
|
|
# Update the properties of the service if needed
|
|
$setServicePropertyParameters = @{}
|
|
|
|
$servicePropertyParameterNames = @( 'StartupType', 'BuiltInAccount', 'Credential', 'DesktopInteract', 'DisplayName', 'Description', 'Dependencies' )
|
|
|
|
foreach ($servicePropertyParameterName in $servicePropertyParameterNames)
|
|
{
|
|
if ($PSBoundParameters.ContainsKey($servicePropertyParameterName))
|
|
{
|
|
$setServicePropertyParameters[$servicePropertyParameterName] = $PSBoundParameters[$servicePropertyParameterName]
|
|
}
|
|
}
|
|
|
|
if ($setServicePropertyParameters.Count -gt 0)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.EditingServiceProperties -f $Name)
|
|
Set-ServiceProperty -ServiceName $Name @setServicePropertyParameters
|
|
}
|
|
|
|
# Update service state if needed
|
|
if ($State -eq 'Stopped')
|
|
{
|
|
Stop-ServiceWithTimeout -ServiceName $Name -TerminateTimeout $TerminateTimeout
|
|
}
|
|
elseif ($State -eq 'Running')
|
|
{
|
|
if ($serviceRestartNeeded)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.RestartingService -f $Name)
|
|
Stop-ServiceWithTimeout -ServiceName $Name -TerminateTimeout $TerminateTimeout
|
|
}
|
|
|
|
Start-ServiceWithTimeout -ServiceName $Name -StartupTimeout $StartupTimeout
|
|
}
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Tests if the service with the given name has the specified property values.
|
|
|
|
.PARAMETER Name
|
|
The name of the service to test.
|
|
|
|
This may be different from the service's display name.
|
|
To retrieve a list of all services with their names and current states, use the Get-Service
|
|
cmdlet.
|
|
|
|
.PARAMETER Ensure
|
|
Specifies whether the service should exist or not.
|
|
|
|
Set this property to Present to test if a service exists.
|
|
Set this property to Absent to test if a service does not exist.
|
|
|
|
The default value is Present.
|
|
|
|
.PARAMETER Path
|
|
The path to the executable the service should be running.
|
|
|
|
.PARAMETER StartupType
|
|
The startup type the service should have.
|
|
|
|
.PARAMETER BuiltInAccount
|
|
The account the service should be starting under.
|
|
|
|
Cannot be specified at the same time as Credential.
|
|
|
|
.PARAMETER DesktopInteract
|
|
Indicates whether or not the service should be able to communicate with a window on the
|
|
desktop.
|
|
|
|
Should be false for services not running as LocalSystem.
|
|
The default value is false.
|
|
|
|
.PARAMETER State
|
|
The state the service should be in.
|
|
The default value is Running.
|
|
|
|
To disregard the state that the service is in, specify this property as Ignore.
|
|
|
|
.PARAMETER DisplayName
|
|
The display name the service should have.
|
|
|
|
.PARAMETER Description
|
|
The description the service should have.
|
|
|
|
.PARAMETER Dependencies
|
|
An array of the names of the dependencies the service should have.
|
|
|
|
.PARAMETER StartupTimeout
|
|
Not used in Test-TargetResource.
|
|
|
|
.PARAMETER TerminateTimeout
|
|
Not used in Test-TargetResource.
|
|
|
|
.PARAMETER Credential
|
|
The credential the service should be running under.
|
|
|
|
Cannot be specified at the same time as BuiltInAccount.
|
|
#>
|
|
function Test-TargetResource
|
|
{
|
|
[OutputType([System.Boolean])]
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Name,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('Present', 'Absent')]
|
|
[System.String]
|
|
$Ensure = 'Present',
|
|
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Path,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('Automatic', 'Manual', 'Disabled')]
|
|
[System.String]
|
|
$StartupType,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
|
|
[System.String]
|
|
$BuiltInAccount,
|
|
|
|
[Parameter()]
|
|
[System.Boolean]
|
|
$DesktopInteract = $false,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('Running', 'Stopped', 'Ignore')]
|
|
[System.String]
|
|
$State = 'Running',
|
|
|
|
[Parameter()]
|
|
[ValidateNotNull()]
|
|
[System.String]
|
|
$DisplayName,
|
|
|
|
[Parameter()]
|
|
[System.String]
|
|
[AllowEmptyString()]
|
|
$Description,
|
|
|
|
[Parameter()]
|
|
[System.String[]]
|
|
[AllowEmptyCollection()]
|
|
$Dependencies,
|
|
|
|
[Parameter()]
|
|
[System.UInt32]
|
|
$StartupTimeout = 30000,
|
|
|
|
[Parameter()]
|
|
[System.UInt32]
|
|
$TerminateTimeout = 30000,
|
|
|
|
[Parameter()]
|
|
[ValidateNotNull()]
|
|
[System.Management.Automation.PSCredential]
|
|
[System.Management.Automation.Credential()]
|
|
$Credential
|
|
)
|
|
|
|
if ($PSBoundParameters.ContainsKey('StartupType'))
|
|
{
|
|
Assert-NoStartupTypeStateConflict -ServiceName $Name -StartupType $StartupType -State $State
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('BuiltInAccount') -and $PSBoundParameters.ContainsKey('Credential'))
|
|
{
|
|
$errorMessage = $script:localizedData.BuiltInAccountAndCredentialSpecified -f $Name
|
|
New-InvalidArgumentException -ArgumentName 'BuiltInAccount & Credential' -Message $errorMessage
|
|
}
|
|
|
|
$serviceResource = Get-TargetResource -Name $Name
|
|
|
|
if ($serviceResource.Ensure -eq 'Absent')
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceDoesNotExist -f $Name)
|
|
return ($Ensure -eq 'Absent')
|
|
}
|
|
else
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceExists -f $Name)
|
|
|
|
if ($Ensure -eq 'Absent')
|
|
{
|
|
return $false
|
|
}
|
|
|
|
# Check the service path
|
|
if ($PSBoundParameters.ContainsKey('Path'))
|
|
{
|
|
$pathsMatch = Test-PathsMatch -ExpectedPath $Path -ActualPath $serviceResource.Path
|
|
|
|
if (-not $pathsMatch)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePathDoesNotMatch -f $Name, $Path, $serviceResource.Path)
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# Check the service display name
|
|
if ($PSBoundParameters.ContainsKey('DisplayName') -and $serviceResource.DisplayName -ine $DisplayName)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'DisplayName', $Name, $DisplayName, $serviceResource.DisplayName)
|
|
return $false
|
|
}
|
|
|
|
# Check the service description
|
|
if ($PSBoundParameters.ContainsKey('Description') -and $serviceResource.Description -ine $Description)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'Description', $Name, $Description, $serviceResource.Description)
|
|
return $false
|
|
}
|
|
|
|
# Check the service dependencies
|
|
if ($PSBoundParameters.ContainsKey('Dependencies'))
|
|
{
|
|
$serviceDependenciesDoNotMatch = $false
|
|
|
|
if ($null -eq $serviceResource.Dependencies -xor $null -eq $Dependencies)
|
|
{
|
|
$serviceDependenciesDoNotMatch = $true
|
|
}
|
|
elseif ($null -ne $serviceResource.Dependencies -and $null -ne $Dependencies)
|
|
{
|
|
$mismatchedDependencies = Compare-Object -ReferenceObject $serviceResource.Dependencies -DifferenceObject $Dependencies
|
|
$serviceDependenciesDoNotMatch = $null -ne $mismatchedDependencies
|
|
}
|
|
|
|
if ($serviceDependenciesDoNotMatch)
|
|
{
|
|
$expectedDependenciesString = $Dependencies -join ','
|
|
$actualDependenciesString = $serviceResource.Dependencies -join ','
|
|
|
|
Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'Dependencies', $Name, $expectedDependenciesString, $actualDependenciesString)
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# Check the service desktop interation setting
|
|
if ($PSBoundParameters.ContainsKey('DesktopInteract') -and $serviceResource.DesktopInteract -ine $DesktopInteract)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'DesktopInteract', $Name, $DesktopInteract, $serviceResource.DesktopInteract)
|
|
return $false
|
|
}
|
|
|
|
# Check the service account properties
|
|
if ($PSBoundParameters.ContainsKey('BuiltInAccount') -and $serviceResource.BuiltInAccount -ine $BuiltInAccount)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'BuiltInAccount', $Name, $BuiltInAccount, $serviceResource.BuiltInAccount)
|
|
return $false
|
|
}
|
|
elseif ($PSBoundParameters.ContainsKey('Credential'))
|
|
{
|
|
$expectedStartName = ConvertTo-StartName -Username $Credential.UserName
|
|
|
|
if ($serviceResource.BuiltInAccount -ine $expectedStartName)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceCredentialDoesNotMatch -f $Name, $Credential.UserName, $serviceResource.BuiltInAccount)
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# Check the service startup type
|
|
if ($PSBoundParameters.ContainsKey('StartupType') -and $serviceResource.StartupType -ine $StartupType)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'StartupType', $Name, $StartupType, $serviceResource.StartupType)
|
|
return $false
|
|
}
|
|
|
|
# Check the service state
|
|
if ($State -ne 'Ignore' -and $serviceResource.State -ine $State)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'State', $Name, $State, $serviceResource.State)
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Retrieves the CIM instance of the service with the given name.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service to get the CIM instance of.
|
|
#>
|
|
function Get-ServiceCimInstance
|
|
{
|
|
[OutputType([Microsoft.Management.Infrastructure.CimInstance])]
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullorEmpty()]
|
|
$ServiceName
|
|
)
|
|
|
|
return Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$ServiceName'"
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Converts the StartMode value returned in a CIM instance of a service to the format
|
|
expected by this resource.
|
|
|
|
.PARAMETER StartMode
|
|
The StartMode value to convert.
|
|
#>
|
|
function ConvertTo-StartupTypeString
|
|
{
|
|
[OutputType([System.String])]
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('Auto', 'Manual', 'Disabled')]
|
|
[System.String]
|
|
$StartMode
|
|
)
|
|
|
|
if ($StartMode -eq 'Auto')
|
|
{
|
|
return 'Automatic'
|
|
}
|
|
|
|
return $StartMode
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Throws an invalid argument error if the given service startup type conflicts with the given
|
|
service state.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service for the error message.
|
|
|
|
.PARAMETER StartupType
|
|
The service startup type to check.
|
|
|
|
.PARAMETER State
|
|
The service state to check.
|
|
#>
|
|
function Assert-NoStartupTypeStateConflict
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('Automatic', 'Manual', 'Disabled')]
|
|
[System.String]
|
|
$StartupType,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('Running', 'Stopped', 'Ignore')]
|
|
[System.String]
|
|
$State
|
|
)
|
|
|
|
if ($State -eq 'Stopped')
|
|
{
|
|
if ($StartupType -eq 'Automatic')
|
|
{
|
|
# Cannot stop a service and set it to start automatically at the same time
|
|
$errorMessage = $script:localizedData.StartupTypeStateConflict -f $ServiceName, $StartupType, $State
|
|
New-InvalidArgumentException -ArgumentName 'StartupType and State' -Message $errorMessage
|
|
}
|
|
}
|
|
elseif ($State -eq 'Running')
|
|
{
|
|
if ($StartupType -eq 'Disabled')
|
|
{
|
|
# Cannot start a service and disable it at the same time
|
|
$errorMessage = $script:localizedData.StartupTypeStateConflict -f $ServiceName, $StartupType, $State
|
|
New-InvalidArgumentException -ArgumentName 'StartupType and State' -Message $errorMessage
|
|
}
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Tests if the two given paths match.
|
|
|
|
.PARAMETER ExpectedPath
|
|
The expected path to test against.
|
|
|
|
.PARAMETER ActualPath
|
|
The actual path to test.
|
|
#>
|
|
function Test-PathsMatch
|
|
{
|
|
[OutputType([System.Boolean])]
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ExpectedPath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ActualPath
|
|
)
|
|
|
|
return (0 -eq [System.String]::Compare($ExpectedPath, $ActualPath, [System.Globalization.CultureInfo]::CurrentUICulture))
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Converts the given username to the string version of it that would be expected in a
|
|
service's StartName property.
|
|
|
|
.PARAMETER Username
|
|
The username to convert.
|
|
#>
|
|
function ConvertTo-StartName
|
|
{
|
|
[OutputType([System.String])]
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Username
|
|
)
|
|
|
|
$startName = $Username
|
|
|
|
if ($Username -ieq 'NetworkService' -or $Username -ieq 'LocalService')
|
|
{
|
|
$startName = "NT Authority\$Username"
|
|
}
|
|
elseif (-not $Username.Contains('\') -and -not $Username.Contains('@'))
|
|
{
|
|
$startName = ".\$Username"
|
|
}
|
|
elseif ($Username.StartsWith("$env:computerName\"))
|
|
{
|
|
$startName = $Username.Replace($env:computerName, '.')
|
|
}
|
|
|
|
return $startName
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Sets the executable path of the service with the given name.
|
|
Returns a boolean specifying whether a restart is needed or not.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service to set the path of.
|
|
|
|
.PARAMETER Path
|
|
The path to set for the service.
|
|
|
|
.NOTES
|
|
SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess.
|
|
This function calls Invoke-CimMethod directly.
|
|
#>
|
|
function Set-ServicePath
|
|
{
|
|
[OutputType([System.Boolean])]
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Path
|
|
)
|
|
|
|
$serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName
|
|
|
|
$pathsMatch = Test-PathsMatch -ExpectedPath $Path -ActualPath $serviceCimInstance.PathName
|
|
|
|
if ($pathsMatch)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePathMatches -f $ServiceName)
|
|
return $false
|
|
}
|
|
else
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServicePathDoesNotMatch -f $ServiceName)
|
|
|
|
$changeServiceArguments = @{
|
|
PathName = $Path
|
|
}
|
|
|
|
$changeServiceResult = Invoke-CimMethod `
|
|
-InputObject $serviceCimInstance `
|
|
-MethodName 'Change' `
|
|
-Arguments $changeServiceArguments
|
|
|
|
if ($changeServiceResult.ReturnValue -ne 0)
|
|
{
|
|
$serviceChangePropertyString = $changeServiceArguments.Keys -join ', '
|
|
$errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeServiceResult.ReturnValue
|
|
New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage
|
|
}
|
|
|
|
return $true
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Sets the dependencies of the service with the given name.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service to set the dependencies of.
|
|
|
|
.PARAMETER Dependencies
|
|
The names of the dependencies to set for the service.
|
|
|
|
.NOTES
|
|
SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess.
|
|
This function calls Invoke-CimMethod directly.
|
|
#>
|
|
function Set-ServiceDependency
|
|
{
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[System.String[]]
|
|
[AllowEmptyCollection()]
|
|
$Dependencies
|
|
)
|
|
|
|
$service = Get-Service -Name $ServiceName -ErrorAction 'SilentlyContinue'
|
|
|
|
$serviceDependenciesMatch = $true
|
|
|
|
$noActualServiceDependencies = $null -eq $service.ServicesDependedOn -or 0 -eq $service.ServicesDependedOn.Count
|
|
$noExpectedServiceDependencies = $null -eq $Dependencies -or 0 -eq $Dependencies.Count
|
|
|
|
if ($noActualServiceDependencies -xor $noExpectedServiceDependencies)
|
|
{
|
|
$serviceDependenciesMatch = $false
|
|
}
|
|
elseif (-not $noActualServiceDependencies -and -not $noExpectedServiceDependencies)
|
|
{
|
|
$mismatchedDependencies = Compare-Object -ReferenceObject $service.ServicesDependedOn.Name -DifferenceObject $Dependencies
|
|
$serviceDependenciesMatch = $null -eq $mismatchedDependencies
|
|
}
|
|
|
|
if ($serviceDependenciesMatch)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceDepdenciesMatch -f $ServiceName)
|
|
}
|
|
else
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceDepdenciesDoNotMatch -f $ServiceName)
|
|
|
|
$serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName
|
|
|
|
$changeServiceArguments = @{
|
|
ServiceDependencies = $Dependencies
|
|
}
|
|
|
|
$changeServiceResult = Invoke-CimMethod `
|
|
-InputObject $serviceCimInstance `
|
|
-MethodName 'Change' `
|
|
-Arguments $changeServiceArguments
|
|
|
|
if ($changeServiceResult.ReturnValue -ne 0)
|
|
{
|
|
$serviceChangePropertyString = $changeServiceArguments.Keys -join ', '
|
|
$errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeServiceResult.ReturnValue
|
|
New-InvalidArgumentException -Message $errorMessage -ArgumentName 'Dependencies'
|
|
}
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Grants the 'Log on as a service' right to the user with the given username.
|
|
|
|
.PARAMETER Username
|
|
The username of the user to grant 'Log on as a service' right to
|
|
#>
|
|
function Grant-LogOnAsServiceRight
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Username
|
|
)
|
|
|
|
$logOnAsServiceText = @"
|
|
namespace LogOnAsServiceHelper
|
|
{
|
|
using Microsoft.Win32.SafeHandles;
|
|
using System;
|
|
using System.Runtime.ConstrainedExecution;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security;
|
|
|
|
public class NativeMethods
|
|
{
|
|
#region constants
|
|
// from ntlsa.h
|
|
private const int POLICY_LOOKUP_NAMES = 0x00000800;
|
|
private const int POLICY_CREATE_ACCOUNT = 0x00000010;
|
|
private const uint ACCOUNT_ADJUST_SYSTEM_ACCESS = 0x00000008;
|
|
private const uint ACCOUNT_VIEW = 0x00000001;
|
|
private const uint SECURITY_ACCESS_SERVICE_LOGON = 0x00000010;
|
|
|
|
// from LsaUtils.h
|
|
private const uint STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034;
|
|
|
|
// from lmcons.h
|
|
private const int UNLEN = 256;
|
|
private const int DNLEN = 15;
|
|
|
|
// Extra characteres for '\', '@' etc.
|
|
private const int EXTRA_LENGTH = 3;
|
|
#endregion constants
|
|
|
|
#region interop structures
|
|
/// <summary>
|
|
/// Used to open a policy, but not containing anything meaqningful
|
|
/// </summary>
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct LSA_OBJECT_ATTRIBUTES
|
|
{
|
|
public UInt32 Length;
|
|
public IntPtr RootDirectory;
|
|
public IntPtr ObjectName;
|
|
public UInt32 Attributes;
|
|
public IntPtr SecurityDescriptor;
|
|
public IntPtr SecurityQualityOfService;
|
|
|
|
public void Initialize()
|
|
{
|
|
this.Length = 0;
|
|
this.RootDirectory = IntPtr.Zero;
|
|
this.ObjectName = IntPtr.Zero;
|
|
this.Attributes = 0;
|
|
this.SecurityDescriptor = IntPtr.Zero;
|
|
this.SecurityQualityOfService = IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// LSA string
|
|
/// </summary>
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
private struct LSA_UNICODE_STRING
|
|
{
|
|
internal ushort Length;
|
|
internal ushort MaximumLength;
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
internal string Buffer;
|
|
|
|
internal void Set(string src)
|
|
{
|
|
this.Buffer = src;
|
|
this.Length = (ushort)(src.Length * sizeof(char));
|
|
this.MaximumLength = (ushort)(this.Length + sizeof(char));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Structure used as the last parameter for LSALookupNames
|
|
/// </summary>
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct LSA_TRANSLATED_SID2
|
|
{
|
|
public uint Use;
|
|
public IntPtr SID;
|
|
public int DomainIndex;
|
|
public uint Flags;
|
|
};
|
|
#endregion interop structures
|
|
|
|
#region safe handles
|
|
/// <summary>
|
|
/// Handle for LSA objects including Policy and Account
|
|
/// </summary>
|
|
private class LsaSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
|
|
{
|
|
[DllImport("advapi32.dll")]
|
|
private static extern uint LsaClose(IntPtr ObjectHandle);
|
|
|
|
/// <summary>
|
|
/// Prevents a default instance of the LsaPolicySafeHAndle class from being created.
|
|
/// </summary>
|
|
private LsaSafeHandle(): base(true)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls NativeMethods.CloseHandle(handle)
|
|
/// </summary>
|
|
/// <returns>the return of NativeMethods.CloseHandle(handle)</returns>
|
|
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
|
protected override bool ReleaseHandle()
|
|
{
|
|
long returnValue = LsaSafeHandle.LsaClose(this.handle);
|
|
return returnValue != 0;
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle for IntPtrs returned from Lsa calls that have to be freed with
|
|
/// LsaFreeMemory
|
|
/// </summary>
|
|
private class SafeLsaMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid
|
|
{
|
|
[DllImport("advapi32")]
|
|
internal static extern int LsaFreeMemory(IntPtr Buffer);
|
|
|
|
private SafeLsaMemoryHandle() : base(true) { }
|
|
|
|
private SafeLsaMemoryHandle(IntPtr handle)
|
|
: base(true)
|
|
{
|
|
SetHandle(handle);
|
|
}
|
|
|
|
private static SafeLsaMemoryHandle InvalidHandle
|
|
{
|
|
get { return new SafeLsaMemoryHandle(IntPtr.Zero); }
|
|
}
|
|
|
|
override protected bool ReleaseHandle()
|
|
{
|
|
return SafeLsaMemoryHandle.LsaFreeMemory(handle) == 0;
|
|
}
|
|
|
|
internal IntPtr Memory
|
|
{
|
|
get
|
|
{
|
|
return this.handle;
|
|
}
|
|
}
|
|
}
|
|
#endregion safe handles
|
|
|
|
#region interop function declarations
|
|
/// <summary>
|
|
/// Opens LSA Policy
|
|
/// </summary>
|
|
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
|
|
private static extern uint LsaOpenPolicy(
|
|
IntPtr SystemName,
|
|
ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
|
|
uint DesiredAccess,
|
|
out LsaSafeHandle PolicyHandle
|
|
);
|
|
|
|
/// <summary>
|
|
/// Convert the name into a SID which is used in remaining calls
|
|
/// </summary>
|
|
[DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true), SuppressUnmanagedCodeSecurityAttribute]
|
|
private static extern uint LsaLookupNames2(
|
|
LsaSafeHandle PolicyHandle,
|
|
uint Flags,
|
|
uint Count,
|
|
LSA_UNICODE_STRING[] Names,
|
|
out SafeLsaMemoryHandle ReferencedDomains,
|
|
out SafeLsaMemoryHandle Sids
|
|
);
|
|
|
|
/// <summary>
|
|
/// Opens the LSA account corresponding to the user's SID
|
|
/// </summary>
|
|
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
|
|
private static extern uint LsaOpenAccount(
|
|
LsaSafeHandle PolicyHandle,
|
|
IntPtr Sid,
|
|
uint Access,
|
|
out LsaSafeHandle AccountHandle);
|
|
|
|
/// <summary>
|
|
/// Creates an LSA account corresponding to the user's SID
|
|
/// </summary>
|
|
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
|
|
private static extern uint LsaCreateAccount(
|
|
LsaSafeHandle PolicyHandle,
|
|
IntPtr Sid,
|
|
uint Access,
|
|
out LsaSafeHandle AccountHandle);
|
|
|
|
/// <summary>
|
|
/// Gets the LSA Account access
|
|
/// </summary>
|
|
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
|
|
private static extern uint LsaGetSystemAccessAccount(
|
|
LsaSafeHandle AccountHandle,
|
|
out uint SystemAccess);
|
|
|
|
/// <summary>
|
|
/// Sets the LSA Account access
|
|
/// </summary>
|
|
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
|
|
private static extern uint LsaSetSystemAccessAccount(
|
|
LsaSafeHandle AccountHandle,
|
|
uint SystemAccess);
|
|
#endregion interop function declarations
|
|
|
|
/// <summary>
|
|
/// Sets the Log On As A Service Policy for <paramref name="userName"/>, if not already set.
|
|
/// </summary>
|
|
/// <param name="userName">the user name we want to allow logging on as a service</param>
|
|
/// <exception cref="ArgumentNullException">If the <paramref name="userName"/> is null or empty.</exception>
|
|
/// <exception cref="InvalidOperationException">In the following cases:
|
|
/// Failure opening the LSA Policy.
|
|
/// The <paramref name="userName"/> is too large.
|
|
/// Failure looking up the user name.
|
|
/// Failure opening LSA account (other than account not found).
|
|
/// Failure creating LSA account.
|
|
/// Failure getting LSA account policy access.
|
|
/// Failure setting LSA account policy access.
|
|
/// </exception>
|
|
public static void SetLogOnAsServicePolicy(string userName)
|
|
{
|
|
if (String.IsNullOrEmpty(userName))
|
|
{
|
|
throw new ArgumentNullException("userName");
|
|
}
|
|
|
|
LSA_OBJECT_ATTRIBUTES objectAttributes = new LSA_OBJECT_ATTRIBUTES();
|
|
objectAttributes.Initialize();
|
|
|
|
// All handles are delcared in advance so they can be closed on finally
|
|
LsaSafeHandle policyHandle = null;
|
|
SafeLsaMemoryHandle referencedDomains = null;
|
|
SafeLsaMemoryHandle sids = null;
|
|
LsaSafeHandle accountHandle = null;
|
|
|
|
try
|
|
{
|
|
uint status = LsaOpenPolicy(
|
|
IntPtr.Zero,
|
|
ref objectAttributes,
|
|
POLICY_LOOKUP_NAMES | POLICY_CREATE_ACCOUNT,
|
|
out policyHandle);
|
|
|
|
if (status != 0)
|
|
{
|
|
throw new InvalidOperationException("CannotOpenPolicyErrorMessage");
|
|
}
|
|
|
|
// Unicode strings have a maximum length of 32KB. We don't want to create
|
|
// LSA strings with more than that. User lengths are much smaller so this check
|
|
// ensures userName's length is useful
|
|
if (userName.Length > UNLEN + DNLEN + EXTRA_LENGTH)
|
|
{
|
|
throw new InvalidOperationException("UserNameTooLongErrorMessage");
|
|
}
|
|
|
|
LSA_UNICODE_STRING lsaUserName = new LSA_UNICODE_STRING();
|
|
lsaUserName.Set(userName);
|
|
|
|
LSA_UNICODE_STRING[] names = new LSA_UNICODE_STRING[1];
|
|
names[0].Set(userName);
|
|
|
|
status = LsaLookupNames2(
|
|
policyHandle,
|
|
0,
|
|
1,
|
|
new LSA_UNICODE_STRING[] { lsaUserName },
|
|
out referencedDomains,
|
|
out sids);
|
|
|
|
if (status != 0)
|
|
{
|
|
throw new InvalidOperationException("CannotLookupNamesErrorMessage");
|
|
}
|
|
|
|
LSA_TRANSLATED_SID2 sid = (LSA_TRANSLATED_SID2)Marshal.PtrToStructure(sids.Memory, typeof(LSA_TRANSLATED_SID2));
|
|
|
|
status = LsaOpenAccount(policyHandle,
|
|
sid.SID,
|
|
ACCOUNT_VIEW | ACCOUNT_ADJUST_SYSTEM_ACCESS,
|
|
out accountHandle);
|
|
|
|
uint currentAccess = 0;
|
|
|
|
if (status == 0)
|
|
{
|
|
status = LsaGetSystemAccessAccount(accountHandle, out currentAccess);
|
|
|
|
if (status != 0)
|
|
{
|
|
throw new InvalidOperationException("CannotGetAccountAccessErrorMessage");
|
|
}
|
|
|
|
}
|
|
else if (status == STATUS_OBJECT_NAME_NOT_FOUND)
|
|
{
|
|
status = LsaCreateAccount(
|
|
policyHandle,
|
|
sid.SID,
|
|
ACCOUNT_ADJUST_SYSTEM_ACCESS,
|
|
out accountHandle);
|
|
|
|
if (status != 0)
|
|
{
|
|
throw new InvalidOperationException("CannotCreateAccountAccessErrorMessage");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("CannotOpenAccountErrorMessage");
|
|
}
|
|
|
|
if ((currentAccess & SECURITY_ACCESS_SERVICE_LOGON) == 0)
|
|
{
|
|
status = LsaSetSystemAccessAccount(
|
|
accountHandle,
|
|
currentAccess | SECURITY_ACCESS_SERVICE_LOGON);
|
|
if (status != 0)
|
|
{
|
|
throw new InvalidOperationException("CannotSetAccountAccessErrorMessage");
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (policyHandle != null) { policyHandle.Close(); }
|
|
if (referencedDomains != null) { referencedDomains.Close(); }
|
|
if (sids != null) { sids.Close(); }
|
|
if (accountHandle != null) { accountHandle.Close(); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"@
|
|
|
|
try
|
|
{
|
|
$null = [LogOnAsServiceHelper.NativeMethods]
|
|
}
|
|
catch
|
|
{
|
|
$logOnAsServiceText = $logOnAsServiceText.Replace('CannotOpenPolicyErrorMessage', `
|
|
$script:localizedData.CannotOpenPolicyErrorMessage)
|
|
$logOnAsServiceText = $logOnAsServiceText.Replace('UserNameTooLongErrorMessage', `
|
|
$script:localizedData.UserNameTooLongErrorMessage)
|
|
$logOnAsServiceText = $logOnAsServiceText.Replace('CannotLookupNamesErrorMessage', `
|
|
$script:localizedData.CannotLookupNamesErrorMessage)
|
|
$logOnAsServiceText = $logOnAsServiceText.Replace('CannotOpenAccountErrorMessage', `
|
|
$script:localizedData.CannotOpenAccountErrorMessage)
|
|
$logOnAsServiceText = $logOnAsServiceText.Replace('CannotCreateAccountAccessErrorMessage', `
|
|
$script:localizedData.CannotCreateAccountAccessErrorMessage)
|
|
$logOnAsServiceText = $logOnAsServiceText.Replace('CannotGetAccountAccessErrorMessage', `
|
|
$script:localizedData.CannotGetAccountAccessErrorMessage)
|
|
$logOnAsServiceText = $logOnAsServiceText.Replace('CannotSetAccountAccessErrorMessage', `
|
|
$script:localizedData.CannotSetAccountAccessErrorMessage)
|
|
$null = Add-Type $logOnAsServiceText -PassThru
|
|
}
|
|
|
|
if ($Username.StartsWith('.\'))
|
|
{
|
|
$Username = $Username.Substring(2)
|
|
}
|
|
|
|
try
|
|
{
|
|
[LogOnAsServiceHelper.NativeMethods]::SetLogOnAsServicePolicy($Username)
|
|
}
|
|
catch
|
|
{
|
|
$errorMessage = $script:localizedData.ErrorSettingLogOnAsServiceRightsForUser -f $Username, $_.Exception.Message
|
|
New-InvalidOperationException -Message $errorMessage
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Sets the service properties involving the account the service is running under.
|
|
(StartName, StartPassword, DesktopInteract)
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service to change the start name of.
|
|
|
|
.PARAMETER BuiltInAccount
|
|
The name of the built-in account to run the service under.
|
|
This value will overwrite the Credential value if Credential is also declared.
|
|
|
|
.PARAMETER Credential
|
|
The user credential to run the service under.
|
|
BuiltInAccount will overwrite this value if BuiltInAccount is also declared.
|
|
|
|
.PARAMETER DesktopInteract
|
|
Indicates whether or not the service should be able to communicate with a window on the
|
|
desktop.
|
|
|
|
Must be false for services not running as LocalSystem.
|
|
|
|
.NOTES
|
|
DesktopInteract is included here because it can only be enabled when the service startup
|
|
account name is LocalSystem. In order not to run into a conflict where one property has
|
|
been updated before the other, both are updated here at the same time.
|
|
|
|
SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess.
|
|
This function calls Invoke-CimMethod directly.
|
|
#>
|
|
function Set-ServiceAccountProperty
|
|
{
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter()]
|
|
[System.String]
|
|
[ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
|
|
$BuiltInAccount,
|
|
|
|
[Parameter()]
|
|
[System.Management.Automation.PSCredential]
|
|
[System.Management.Automation.Credential()]
|
|
$Credential,
|
|
|
|
[Parameter()]
|
|
[System.Boolean]
|
|
$DesktopInteract
|
|
)
|
|
|
|
$serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName
|
|
|
|
$changeServiceArguments = @{}
|
|
|
|
if ($PSBoundParameters.ContainsKey('BuiltInAccount'))
|
|
{
|
|
$startName = ConvertTo-StartName -Username $BuiltInAccount
|
|
|
|
if ($serviceCimInstance.StartName -ine $startName)
|
|
{
|
|
$changeServiceArguments['StartName'] = $startName
|
|
$changeServiceArguments['StartPassword'] = ''
|
|
}
|
|
}
|
|
elseif ($PSBoundParameters.ContainsKey('Credential'))
|
|
{
|
|
$startName = ConvertTo-StartName -Username $Credential.UserName
|
|
|
|
if ($serviceCimInstance.StartName -ine $startName)
|
|
{
|
|
Grant-LogOnAsServiceRight -Username $startName
|
|
|
|
$changeServiceArguments['StartName'] = $startName
|
|
$changeServiceArguments['StartPassword'] = $Credential.GetNetworkCredential().Password
|
|
}
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('DesktopInteract'))
|
|
{
|
|
if ($serviceCimInstance.DesktopInteract -ne $DesktopInteract)
|
|
{
|
|
$changeServiceArguments['DesktopInteract'] = $DesktopInteract
|
|
}
|
|
}
|
|
|
|
if ($changeServiceArguments.Count -gt 0)
|
|
{
|
|
$changeServiceResult = Invoke-CimMethod -InputObject $ServiceCimInstance -MethodName 'Change' -Arguments $changeServiceArguments
|
|
|
|
if ($changeServiceResult.ReturnValue -ne 0)
|
|
{
|
|
$serviceChangePropertyString = $changeServiceArguments.Keys -join ', '
|
|
$errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeServiceResult.ReturnValue
|
|
New-InvalidArgumentException -ArgumentName 'BuiltInAccount, Credential, or DesktopInteract' -Message $errorMessage
|
|
}
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Sets the startup type of the service with the given name.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service to set the startup type of.
|
|
|
|
.PARAMETER StartupType
|
|
The startup type value to set for the service.
|
|
|
|
.NOTES
|
|
SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess.
|
|
This function calls Invoke-CimMethod directly.
|
|
#>
|
|
function Set-ServiceStartupType
|
|
{
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('Automatic', 'Manual', 'Disabled')]
|
|
[System.String]
|
|
$StartupType
|
|
)
|
|
|
|
$serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName
|
|
$serviceStartupType = ConvertTo-StartupTypeString -StartMode $serviceCimInstance.StartMode
|
|
|
|
if ($serviceStartupType -ieq $StartupType)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceStartupTypeMatches -f $ServiceName)
|
|
}
|
|
else
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceStartupTypeDoesNotMatch -f $ServiceName)
|
|
|
|
$changeServiceArguments = @{
|
|
StartMode = $StartupType
|
|
}
|
|
|
|
$changeResult = Invoke-CimMethod `
|
|
-InputObject $serviceCimInstance `
|
|
-MethodName 'Change' `
|
|
-Arguments $changeServiceArguments
|
|
|
|
if ($changeResult.ReturnValue -ne 0)
|
|
{
|
|
$serviceChangePropertyString = $changeServiceArguments.Keys -join ', '
|
|
$errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeResult.ReturnValue
|
|
New-InvalidArgumentException -ArgumentName 'StartupType' -Message $errorMessage
|
|
}
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Sets the service with the given name to have the specified properties.
|
|
|
|
.PARAMETER Name
|
|
The name of the service to set the properties of.
|
|
|
|
.PARAMETER DisplayName
|
|
The display name the service should have.
|
|
|
|
.PARAMETER Description
|
|
The description the service should have.
|
|
|
|
.PARAMETER Dependencies
|
|
The names of the dependencies the service should have.
|
|
|
|
.PARAMETER BuiltInAccount
|
|
The built-in account the service should start under.
|
|
|
|
Cannot be specified at the same time as Credential.
|
|
|
|
.PARAMETER Credential
|
|
The credential of the user account the service should start under.
|
|
|
|
Cannot be specified at the same time as BuiltInAccount.
|
|
The user specified by this credential will automatically be granted the Log on as a Service
|
|
right.
|
|
|
|
.PARAMETER DesktopInteract
|
|
Indicates whether or not the service should be able to communicate with a window on the desktop.
|
|
|
|
.PARAMETER StartupType
|
|
The startup type the service should have.
|
|
|
|
.NOTES
|
|
SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess.
|
|
Here are the paths through which Set-ServiceProperty calls Invoke-CimMethod:
|
|
|
|
Set-ServiceProperty --> Set-ServiceDependency --> Invoke-CimMethod
|
|
--> Set-ServieceAccountProperty --> Invoke-CimMethod
|
|
--> Set-ServiceStartupType --> Invoke-CimMethod
|
|
#>
|
|
function Set-ServiceProperty
|
|
{
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('Automatic', 'Manual', 'Disabled')]
|
|
[System.String]
|
|
$StartupType,
|
|
|
|
[Parameter()]
|
|
[ValidateSet('LocalSystem', 'LocalService', 'NetworkService')]
|
|
[System.String]
|
|
$BuiltInAccount,
|
|
|
|
[Parameter()]
|
|
[System.Boolean]
|
|
$DesktopInteract,
|
|
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$DisplayName,
|
|
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Description,
|
|
|
|
[Parameter()]
|
|
[System.String[]]
|
|
[AllowEmptyCollection()]
|
|
$Dependencies,
|
|
|
|
[Parameter()]
|
|
[ValidateNotNull()]
|
|
[System.Management.Automation.PSCredential]
|
|
[System.Management.Automation.Credential()]
|
|
$Credential
|
|
)
|
|
|
|
# Update display name and/or description if needed
|
|
$serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName
|
|
|
|
$setServiceParameters = @{}
|
|
|
|
if ($PSBoundParameters.ContainsKey('DisplayName') -and $serviceCimInstance.DisplayName -ine $DisplayName)
|
|
{
|
|
$setServiceParameters['DisplayName'] = $DisplayName
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('Description') -and $serviceCimInstance.Description -ine $Description)
|
|
{
|
|
$setServiceParameters['Description'] = $Description
|
|
}
|
|
|
|
if ($setServiceParameters.Count -gt 0)
|
|
{
|
|
$null = Set-Service -Name $ServiceName @setServiceParameters
|
|
}
|
|
|
|
# Update service dependencies if needed
|
|
if ($PSBoundParameters.ContainsKey('Dependencies'))
|
|
{
|
|
Set-ServiceDependency -ServiceName $ServiceName -Dependencies $Dependencies
|
|
}
|
|
|
|
# Update service account properties if needed
|
|
$setServiceAccountPropertyParameters = @{}
|
|
|
|
if ($PSBoundParameters.ContainsKey('BuiltInAccount'))
|
|
{
|
|
$setServiceAccountPropertyParameters['BuiltInAccount'] = $BuiltInAccount
|
|
}
|
|
elseif ($PSBoundParameters.ContainsKey('Credential'))
|
|
{
|
|
$setServiceAccountPropertyParameters['Credential'] = $Credential
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('DesktopInteract'))
|
|
{
|
|
$setServiceAccountPropertyParameters['DesktopInteract'] = $DesktopInteract
|
|
}
|
|
|
|
if ($setServiceAccountPropertyParameters.Count -gt 0)
|
|
{
|
|
Set-ServiceAccountProperty -ServiceName $ServiceName @setServiceAccountPropertyParameters
|
|
}
|
|
|
|
# Update startup type
|
|
if ($PSBoundParameters.ContainsKey('StartupType'))
|
|
{
|
|
Set-ServiceStartupType -ServiceName $ServiceName -StartupType $StartupType
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Deletes the service with the given name.
|
|
|
|
This is a wrapper function for unit testing.
|
|
|
|
.PARAMETER Name
|
|
The name of the service to delete.
|
|
#>
|
|
function Remove-Service
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Name
|
|
)
|
|
|
|
& 'sc.exe' 'delete' $Name
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Deletes the service with the given name and waits for the service to be deleted.
|
|
|
|
.PARAMETER Name
|
|
The name of the service to delete.
|
|
|
|
.PARAMETER TerminateTimeout
|
|
The time to wait for the service to be deleted in milliseconds.
|
|
#>
|
|
function Remove-ServiceWithTimeout
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$Name,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[System.UInt32]
|
|
$TerminateTimeout
|
|
)
|
|
|
|
Remove-Service -Name $Name
|
|
|
|
$serviceDeleted = $false
|
|
$start = [System.DateTime]::Now
|
|
|
|
while (-not $serviceDeleted -and ([System.DateTime]::Now - $start).TotalMilliseconds -lt $TerminateTimeout)
|
|
{
|
|
$service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue'
|
|
|
|
if ($null -eq $service)
|
|
{
|
|
$serviceDeleted = $true
|
|
}
|
|
else
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.WaitingForServiceDeletion -f $Name)
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
}
|
|
|
|
if ($serviceDeleted)
|
|
{
|
|
Write-Verbose -Message ($script:localizedData.ServiceDeletionSucceeded -f $Name)
|
|
}
|
|
else
|
|
{
|
|
New-InvalidOperationException -Message ($script:localizedData.ServiceDeletionFailed -f $Name)
|
|
}
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Waits for the service with the given name to reach the given state within the given time
|
|
span.
|
|
|
|
This is a wrapper function for unit testing.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service that should be in the given state.
|
|
|
|
.PARAMETER State
|
|
The state the service should be in.
|
|
|
|
.PARAMETER WaitTimeSpan
|
|
A time span of how long to wait for the service to reach the desired state.
|
|
#>
|
|
function Wait-ServiceStateWithTimeout
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[System.ServiceProcess.ServiceControllerStatus]
|
|
$State,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[System.TimeSpan]
|
|
$WaitTimeSpan
|
|
)
|
|
|
|
$service = Get-Service -Name $ServiceName -ErrorAction 'SilentlyContinue'
|
|
$Service.WaitForStatus($State, $WaitTimeSpan)
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Starts the service with the given name, if it is not already running, and waits for the
|
|
service to be running.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service to start.
|
|
|
|
.PARAMETER StartupTimeout
|
|
The time to wait for the service to be running in milliseconds.
|
|
#>
|
|
function Start-ServiceWithTimeout
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[System.UInt32]
|
|
$StartupTimeout
|
|
)
|
|
|
|
Start-Service -Name $ServiceName
|
|
$waitTimeSpan = New-Object -TypeName 'TimeSpan' -ArgumentList (0, 0, 0, 0, $StartupTimeout)
|
|
Wait-ServiceStateWithTimeout -ServiceName $ServiceName -State 'Running' -WaitTimeSpan $waitTimeSpan
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Stops the service with the given name, if it is not already stopped, and waits for the
|
|
service to be stopped.
|
|
|
|
.PARAMETER ServiceName
|
|
The name of the service to stop.
|
|
|
|
.PARAMETER TerminateTimeout
|
|
The time to wait for the service to be stopped in milliseconds.
|
|
#>
|
|
function Stop-ServiceWithTimeout
|
|
{
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.String]
|
|
$ServiceName,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[System.UInt32]
|
|
$TerminateTimeout
|
|
)
|
|
|
|
Stop-Service -Name $ServiceName
|
|
$waitTimeSpan = New-Object -TypeName 'TimeSpan' -ArgumentList (0, 0, 0, 0, $TerminateTimeout)
|
|
Wait-ServiceStateWithTimeout -ServiceName $ServiceName -State 'Stopped' -WaitTimeSpan $waitTimeSpan
|
|
}
|