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 * |