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)
        }
    }
}