ScriptAsService.psm1

# Import Dependency Libraries
$DependencyRoot = "$PSScriptRoot\Libraries\Sorlov.PowerShell"
Import-Module "$DependencyRoot\Sorlov.PowerShell.Core.psd1"
Import-Module "$DependencyRoot\Sorlov.PowerShell.SelfHosted.dll"

Function Install-ScriptAsService {
    <#
        .SYNOPSIS
         Installs a script-as-service binary to the local computer.
 
        .DESCRIPTION
         Installs a script-as-service binary to the local computer.
 
        .PARAMETER Path
         The full or relative path to the binary file which should be run as a service.
 
        .PARAMETER Name
         The short, terse, unique name of the service - this **CAN NOT** have any spaces in it.
 
        .PARAMETER Description
         The description of the service you are creating. You can, optionally, leave this null.
 
        .PARAMETER Credential
         The credential (username and password) under which the service should run.
         It is preferable to set this to an account with minimal required permissions or managed service account.
 
        .EXAMPLE
         Install-ScripptAsService -Path C:\Project\Out\project.exe -Name Project -DisplayName 'Scheduled Project' -Credential $Cred
 
         This command installs a looping scriptp as a service from the specified path and with the specified name and display name.
 
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,
        HelpMessage="Type the path (full or relative) to the script-as-service binary you want to install; make sure to include the file name and extension.")]
        [ValidateScript({Test-Path -Path $_})]
        [Alias("FilePath","SourceFilePath","ScriptPath")]
        [string]$Path,
        [Parameter(Mandatory=$true,
        HelpMessage="Type the SERVICE NAME [[ this is NOT the display name! ]] `r`nService Name must not contain spaces!")]
        [Alias("ServiceName")]
        [string]$Name,
        [Parameter(Mandatory=$true,
        HelpMessage="Type the desired service description.")]
        [Alias("SvcDescription","ServiceDescription")]
        [string]$Description,
        [Management.Automation.PSCredential]$Credential
    )

    [string]$RegistryPath  = "HKLM:\SYSTEM\CurrentControlSet\Services\$Name"
    [string]$RegistryName  = 'Description'
    [string]$RegistryValue = $Description

    Try {
        $ErrorActionPreferenceHolder = $ErrorActionPreference
        $ErrorActionPreference = "Stop"
        Start-Process -FilePath $Path -ArgumentList "/install" -Wait -WindowStyle Hidden
        $null = New-ItemProperty -Path $RegistryPath -Name $RegistryName -Value $RegistryValue -PropertyType String -Force
        If ($Credential) {
            $null = Set-ServiceCredential -ServiceName $Name -ServiceCredential $Credential
        }
        $null = Set-Service -Name $Name -StartupType Automatic
        $null = Start-Service -Name $Name
        Get-Service -Name $Name
        $ErrorActionPreference = $ErrorActionPreferenceHolder
    } Catch {
        Throw $_
    }
}


Function New-ScriptAsService {
    <#
        .SYNOPSIS
        Creates a Windows Service via .ps1 script.
 
        .DESCRIPTION
        This script is designed to convert at .ps1 looping script into a Windows Service legible binary file (aka .exe).
 
        The script **must** include the looping logic inside itself or the service will fail to run properly.
 
        You _must_ include the path to the script which is to be turned into a service, the destination path for the binary,
        the name of the service, and the display name of the service.
 
        You _can_, optionally, sign your binaries with a code-signing cert and timestamp url.
        You can also give the new binary an icon.
 
        .PARAMETER Path
         The full or relative path to the script file which should be run as a service.
 
        .PARAMETER Destination
         The full or relative path you want the binary to be output to.
         Note that this must include the name and extension of the binary (`C:\Some\Path\foo.exe`) whether you specify a full or relative path.
 
        .PARAMETER Name
         The short, terse, unique name of the service - this **CAN NOT** have any spaces in it.
 
        .PARAMETER DisplayName
         The display name of the service - something human readable and friendly.
         This _can_ include spaces.
 
        .PARAMETER Description
         The description of the service you are creating. You can, optionally, leave this null.
 
        .PARAMETER IconFilePath
         The full or relative path to the icon you want to set for the new service.
         This is optional.
 
        .PARAMETER Version
         The version you want the binary output to have - if you do not specify one, the version defaults to 1.0.0.
         Must be a valid [Semantic Version](semver.org).
 
        .EXAMPLE
         New-ScriptAsService -Path .\Project\project.ps1 -Name Project -DisplayName 'Looping Project'
 
         This will create a script-as-service binary, called `project.exe`, which when installed as a service
         will have a name of `Project` and a display name of `Looping Project`. The description will be empty
         and the version will be `1.0.0`.
 
        .PARAMETER SigningCertificatePath
         The full or relative path to the certificate you want to use for signing your binary.
         Must be a cert valid for code signing.
         This is an optional parameter.
 
        .PARAMETER TimeStampUrl
         If you are signing your binary, you probably also want to provide a timestamp url.
         Otherwise, do not include this parameter.
 
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,
        HelpMessage="Type the path (full or relative) to the Script you want to turn into a service; make sure to include the file name and extension.")]
        [ValidateScript({Test-Path -Path $_})]
        [Alias("FilePath","SourceFilePath","ScriptPath")]
        [string]$Path,

        [Parameter(Mandatory=$true,
        HelpMessage="Type the path (full or relative) to where you want the service executable to be placed; make sure to include the file name and '.exe' extension.")]
        [Alias("OutputFilePath","DestinationPath","BinaryPath")]
        [string]$Destination,

        [Parameter(Mandatory=$true,
        HelpMessage="Type the desired SERVICE NAME [[ this is NOT the display name! ]] `r`nService Name must not contain spaces!")]
        [Alias("SvcName","ServiceName")]
        [string]$Name,

        [Parameter(Mandatory=$true,
        HelpMessage="Type the desired service DISPLAY name.")]
        [Alias("SvcDisplayName","ServiceDisplayName")]
        [string]$DisplayName,

        [Alias("SvcDescription","ServiceDescription")]
        [string]$Description = " ",

        [ValidateScript({Test-Path -Path $_})]
        [Alias("IconPath","Icon")]
        [string]$IconFilePath,

        [version]$Version = "1.0.0",

        [ValidateScript({Test-Path -Path $_})]
        [Alias("CertificatePath","Certificate","CertPath","Cert")]
        [string]$SigningCertificatePath,

        [string]$TimeStampUrl
    )

    $ServiceParameters = @{
        SourceFile         = $Path
        DestinationFile    = $Destination
        Service            = $true
        ServiceDescription = $Description
        ServiceName        = $Name
        ServiceDisplayName = $DisplayName
        Version            = [string]$Version
    }
    
    If (-not [string]::IsNullOrEmpty($SigningCertificatePath)){
        $null = $ServiceParameters.Add("Sign",$true)
        $null = $ServiceParameters.Add("Certificate",$SigningCertificatePath)
        If (-not [string]::IsNullOrEmpty($TimeStampUrl)){
            $null = $ServiceParameters.Add("TimeStampURL",$TimeStampUrl)
        }
    }
    If (-not [string]::IsNullOrEmpty($IconFilePath)){
        $null = $ServiceParameters.Add("IconPath",$IconFilePath)
    }
    New-SelfHostedPS @ServiceParameters
}


function Set-ScriptAsServiceCredential {

    <#
        .SYNOPSIS
         Set the Credential of an installed service.
 
        .DESCRIPTION
         Set or update the credential of an installed service programmatically.
         Sometimes this is required because the username/password of the account has expired or because a new account should be used.
 
        .PARAMETER Name
         The short, terse, unique name of the service - this **CAN NOT** have any spaces in it.
 
        .PARAMETER Credential
         The credential under which the service is being set to run.
 
        .PARAMETER ComputerName
         The ComputerName on which the service is to be updated.
         By default, this command executes against the localmachine.
 
        .EXAMPLE
         Set-ScriptAsServiceCredential -Name MyProject -Credential (Get-Credential SomeAccount)
 
         This command will ask for the credentials for `SomeAccount` and then set the service `MyProject`
         to run under `SomeAccount` using the specified credentials.
 
    #>

    [cmdletbinding()]
    param(
        [String[]]$Name,
        [Management.Automation.PSCredential]$Credential,
        [string]$ComputerName = $env:COMPUTERNAME
    )

    $ServiceQueryParameters = @{
        "Namespace"    = "root\CIMV2"
        "Class"        = "Win32_Service"
        "ComputerName" = $ComputerName
        "Filter"       = "Name='$Name' OR DisplayName='$Name'"
    }
    $Service = Get-WmiObject @ServiceQueryParameters

    if ( -not $Service ) {
        Write-Error "Unable to find service named '$ServiceName' on '$ComputerName'."
    } else {
        # See https://msdn.microsoft.com/en-us/library/aa384901.aspx
        $returnValue = ($Service.Change($null,                       # DisplayName
                $null,                                               # PathName
                $null,                                               # ServiceType
                $null,                                               # ErrorControl
                $null,                                               # StartMode
                $null,                                               # DesktopInteract
                $ServiceCredential.UserName,                         # StartName
                $ServiceCredential.GetNetworkCredential().Password,  # StartPassword
                $null,                                               # LoadOrderGroup
                $null,                                               # LoadOrderGroupDependencies
                $null)).ReturnValue                                  # ServiceDependencies
        $ErrorMessage = "Error setting credentials for service '$ServiceName' on '$ComputerName'"
        switch ( $returnValue ) {
            0  { Write-Verbose "Set credentials for service '$ServiceName' on '$ComputerName'" }
            1  { Write-Error "$ErrorMessage - Not Supported" }
            2  { Write-Error "$ErrorMessage - Access Denied" }
            3  { Write-Error "$ErrorMessage - Dependent Services Running" }
            4  { Write-Error "$ErrorMessage - Invalid Service Control" }
            5  { Write-Error "$ErrorMessage - Service Cannot Accept Control" }
            6  { Write-Error "$ErrorMessage - Service Not Active" }
            7  { Write-Error "$ErrorMessage - Service Request timeout" }
            8  { Write-Error "$ErrorMessage - Unknown Failure" }
            9  { Write-Error "$ErrorMessage - Path Not Found" }
            10 { Write-Error "$ErrorMessage - Service Already Stopped" }
            11 { Write-Error "$ErrorMessage - Service Database Locked" }
            12 { Write-Error "$ErrorMessage - Service Dependency Deleted" }
            13 { Write-Error "$ErrorMessage - Service Dependency Failure" }
            14 { Write-Error "$ErrorMessage - Service Disabled" }
            15 { Write-Error "$ErrorMessage - Service Logon Failed" }
            16 { Write-Error "$ErrorMessage - Service Marked For Deletion" }
            17 { Write-Error "$ErrorMessage - Service No Thread" }
            18 { Write-Error "$ErrorMessage - Status Circular Dependency" }
            19 { Write-Error "$ErrorMessage - Status Duplicate Name" }
            20 { Write-Error "$ErrorMessage - Status Invalid Name" }
            21 { Write-Error "$ErrorMessage - Status Invalid Parameter" }
            22 { Write-Error "$ErrorMessage - Status Invalid Service Account" }
            23 { Write-Error "$ErrorMessage - Status Service Exists" }
            24 { Write-Error "$ErrorMessage - Service Already Paused" }
        }
    }
}


function Uninstall-ScriptAsService {
    <#
        .SYNOPSIS
         Uninstall a script-as-service binary
 
        .DESCRIPTION
         Uninstalls a script-as-service from one or more nodes.
 
        .PARAMETER Name
         The short, terse, unique name of the service - this **CAN NOT** have any spaces in it.
 
        .PARAMETER ComputerName
         The name of the computer or computers from which you want to uninstall the script-as-service
         binary. By default, the command targets the local machine.
 
        .EXAMPLE
         Uninstall-ScriptAsService -Name MyProject
 
         This command will uninstall the script-as-service binary whose service name is `MyProject`.
    #>

    Param(
        [Parameter( Mandatory = $true )]
        [string]$Name,
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    Try {
        $Service = Get-WmiObject -Class win32_service -ComputerName $ComputerName -ErrorAction Stop `
        | Where-Object -FilterScript {$_.Name -eq $Name} -ErrorAction Stop
        $null = $Service.StopService()
        $null = $Service.Delete()
    } Catch {
        Throw $_
    }

    $Service = Get-WmiObject -Class win32_service -ComputerName $ComputerName `
    | Where-Object -FilterScript {$_.Name -eq $Name}

    If (-not [string]::IsNullOrEmpty($Service)){
        Throw "Service not uninstalled!"
    }
}




Export-ModuleMember -Function *