Из любого скрипта PowerShell можно сделать службу Windows, которая работает в фоновом режиме и запускается автоматически при загрузке сервера. Вы можете создать службу Windows с помощью утилит srvany.exe и instsrv.exe (из состава Windows Server Resource 2003 Kit), позволяющих запустить процесс powershell.exe с параметром в виде пути к ps1 файлу скрипта. Основной недостаток такого способа создания службы — srvany.exe не контролирует выполнение приложения (скрипта PowerShell в нашем случае) и, если приложение падает (зависает), то служба это не видит и продолжает работать. В этой статье для создания службы Windows из файла со скриптом PowerShell мы будем использовать утилиту NSSM (Non-Sucking Service Manager – оставим без перевода…:)), которая лишена этих недостатков.
Вы можете скачать и установить NSSM вручную или через Chocolately. Сначала нужно разрешить запуск PS1 скриптов в сесиии и установить сам Choco:
Set-ExecutionPolicy Bypass -Scope Process -Force; `
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
Затем установим пакет NSSM:
choco install nssm
В этом примере мы будем в реальном времени отслеживать изменения определенной группы AD (скрипт из этой статьи) и при изменении оповещать администратора безопасности всплывающим уведомлением и письмом.
Итак, у нас имеется код, который нужно сохранить в PS1 файл. Добавим бесконечный цикл, который раз в минуту выполняет проверку:
while($true) {
#Ваш PS код
Start-Sleep –Seconds 60
}
Конечно, для реализации подобного сценария можно создать и задание в планировщике (Task Scheduler), но если вам нужно реагировать на любые изменения в реальном времени, метод с отдельной службой гораздо правильнее.
Создать службу из скрипта PowerShell при помощи NSSM можно прямо из PowerShell :):
$NSSMPath = (Get-Command "C:\tools\nssm\win64\nssm.exe").Source
$NewServiceName = “CheckADGroupSrv”
$PoShPath= (Get-Command powershell).Source
$PoShScriptPath = “C:\tools\CheckADGroup\checkad.ps1”
$args = '-ExecutionPolicy Bypass -NoProfile -File "{0}"' -f $PoShScriptPath
& $NSSMPath install $NewServiceName $PoShPath $args
& $NSSMPath status $NewServiceName
Запустим новую службу:
Start-Service $NewServiceName
Проверим статус службы с помощью PowerShell:
Get-Service $NewServiceName
Итак, вы создали и запустили новую службу Windows. Проверим, что она появилась в консоли управления службами services.msc
Служба CheckADGroupSrv действительно появилась, она настроена на автоматический запус и в данный момент запущена (Running). Как вы видите, ваш PowerShell скрипт запущен внутри процесса nssm.exe.
Обратите внимание, что служба запущена из-под учетной записи System. Если вы используете в своих PS скриптах другие модули (в моем случае для получения состава доменной группы безопасности используется командлет Get-ADGroupMember из модуля Active Directory для Windows PowerShell), этот аккаунт должен иметь доступ к файлам модуля и права на подключение к AD (в моем случае). Вы так же можете запустить эту службы под другой учётной записью (или аккаунтом gMSA) и предоставить пользователям права на остановку/перезапуск службы, если у них нет прав локального администратора.
Чтобы служба могла отображать уведомления в сеанс пользователя (взаимодействовать с рабочим столом) нужно на вкладке “Вход в систему” (Log on) включить опцию “Разрешить взаимодействие с рабочим столом” (Allow service to interact with desktop).
Чтобы это работало в Windows 10 / Windows Server 2012 R2/ 2016 нужно изменить значение DWORD параметра реестра NoInteractiveServices в ветке HKLM\System\CurrentControlSet\Control\Windows на 0 и включить службу обозревателя интерактивных служб (Interactive Services Detection Service):
Start-Service -Name ui0detect
Однако в Windows 10 1803 службу Interactive Services Detection Service полностью убрали из системы, и вы более не можете переключиться в нулевую сессию (Session 0), так что вы просто не увидите окна, которые выводятся из-под аккаунта System.
Вы можете изменить описание службы командой:
& $NSSMPath set $NewServiceName description “Мониторинг изменений группы AD”
Чтобы удалить созданную службу можете воспользоваться командой sc delete или
nssm remove CheckADGroupSrv
I came across this fantastic script built by jf.larvoire@hpe.com. This script can be used to run a PowerShell script as a service. It has a built-in timer example that fires every X seconds.
JFLarvoire has some other nice scripts, including a Library.ps1 containing various utility scripts available as well. Check the references at the end of this page.
###############################################################################
# #
# File name PSService.ps1 #
# #
# Description A sample service in a standalone PowerShell script #
# #
# Notes The latest PSService.ps1 version is available in GitHub #
# repository https://github.com/JFLarvoire/SysToolsLib/ , #
# in the PowerShell subdirectory. #
# Please report any problem in the Issues tab in that #
# GitHub repository in #
# https://github.com/JFLarvoire/SysToolsLib/issues #
# If you do submit a pull request, please add a comment at #
# the end of this header with the date, your initials, and #
# a description of the changes. Also update $scriptVersion. #
# #
# The initial version of this script was described in an #
# article published in the May 2016 issue of MSDN Magazine. #
# https://msdn.microsoft.com/en-us/magazine/mt703436.aspx #
# This updated version has one major change: #
# The -Service handler in the end has been rewritten to be #
# event-driven, with a second thread waiting for control #
# messages coming in via a named pipe. #
# This allows fixing a bug of the original version, that #
# did not stop properly, and left a zombie process behind. #
# The drawback is that the new code is significantly longer,#
# due to the added PowerShell thread management routines. #
# On the other hand, these thread management routines are #
# reusable, and will allow building much more powerful #
# services. #
# #
# Dynamically generates a small PSService.exe wrapper #
# application, that in turn invokes this PowerShell script. #
# #
# Some arguments are inspired by Linux' service management #
# arguments: -Start, -Stop, -Restart, -Status #
# Others are more in the Windows' style: -Setup, -Remove #
# #
# The actual start and stop operations are done when #
# running as SYSTEM, under the control of the SCM (Service #
# Control Manager). #
# #
# To create your own service, make a copy of this file and #
# rename it. The file base name becomes the service name. #
# Then implement your own service code in the if ($Service) #
# {block} at the very end of this file. See the TO DO #
# comment there. #
# There are global settings below the script param() block. #
# They can easily be changed, but the defaults should be #
# suitable for most projects. #
# #
# Service installation and usage: See the dynamic help #
# section below, or run: help .\PSService.ps1 -Detailed #
# #
# Debugging: The Log function writes messages into a file #
# called C:\Windows\Logs\PSService.log (or actually #
# ${env:windir}\Logs\$serviceName.log). #
# It is very convenient to monitor what's written into that #
# file with a WIN32 port of the Unix tail program. Usage: #
# tail -f C:\Windows\Logs\PSService.log #
# #
# History #
# 2015-07-10 JFL jf.larvoire@hpe.com created this script. #
# 2015-10-13 JFL Made this script completely generic, and added comments #
# in the header above. #
# 2016-01-02 JFL Moved the Event Log name into new variable $logName. #
# Improved comments. #
# 2016-01-05 JFL Fixed the StartPending state reporting. #
# 2016-03-17 JFL Removed aliases. Added missing explicit argument names. #
# 2016-04-16 JFL Moved the official repository on GitHub. #
# 2016-04-21 JFL Minor bug fix: New-EventLog did not use variable $logName.#
# 2016-05-25 JFL Bug fix: The service task was not properly stopped; Its #
# finally block was not executed, and a zombie task often #
# remained. Fixed by using a named pipe to send messages #
# to the service task. #
# 2016-06-05 JFL Finalized the event-driven service handler. #
# Fixed the default command setting in PowerShell v2. #
# Added a sample -Control option using the new pipe. #
# 2016-06-08 JFL Rewrote the pipe handler using PSThreads instead of Jobs. #
# 2016-06-09 JFL Finalized the PSThread management routines error handling.#
# This finally fixes issue #1. #
# 2016-08-22 JFL Fixed issue #3 creating the log and install directories. #
# Thanks Nischl. #
# 2016-09-06 JFL Fixed issue #4 detecting the System account. Now done in #
# a language-independent way. Thanks A Gonzalez. #
# 2016-09-19 JFL Fixed issue #5 starting services that begin with a number.#
# Added a $ServiceDescription string global setting, and #
# use it for the service registration. #
# Added comments about Windows event logs limitations. #
# 2016-11-17 RBM Fixed issue #6 Mangled hyphen in final Unregister-Event. #
# 2017-05-10 CJG Added execution policy bypass flag. #
# 2017-10-04 RBL rblindberg Updated C# code OnStop() routine fixing #
# orphaned process left after stoping the service. #
# 2017-12-05 NWK omrsafetyo Added ServiceUser and ServicePassword to the #
# script parameters. #
# 2017-12-10 JFL Removed the unreliable service account detection tests, #
# and instead use dedicated -SCMStart and -SCMStop #
# arguments in the PSService.exe helper app. #
# Renamed variable userName as currentUserName. #
# Renamed arguments ServiceUser and ServicePassword to the #
# more standard UserName and Password. #
# Also added the standard argument -Credential. #
# #
###############################################################################
#Requires -version 2
<#
.SYNOPSIS
A sample Windows service, in a standalone PowerShell script.
.DESCRIPTION
This script demonstrates how to write a Windows service in pure PowerShell.
It dynamically generates a small PSService.exe wrapper, that in turn
invokes this PowerShell script again for its start and stop events.
.PARAMETER Start
Start the service.
.PARAMETER Stop
Stop the service.
.PARAMETER Restart
Stop then restart the service.
.PARAMETER Status
Get the current service status: Not installed / Stopped / Running
.PARAMETER Setup
Install the service.
Optionally use the -Credential or -UserName arguments to specify the user
account for running the service. By default, uses the LocalSystem account.
Known limitation with the old PowerShell v2: It is necessary to use -Credential
or -UserName. For example, use -UserName LocalSystem to emulate the v3+ default.
.PARAMETER Credential
User and password credential to use for running the service.
For use with the -Setup command.
Generate a PSCredential variable with the Get-Credential command.
.PARAMETER UserName
User account to use for running the service.
For use with the -Setup command, in the absence of a Credential variable.
The user must have the "Log on as a service" right. To give him that right,
open the Local Security Policy management console, go to the
"\Security Settings\Local Policies\User Rights Assignments" folder, and edit
the "Log on as a service" policy there.
Services should always run using a user account which has the least amount
of privileges necessary to do its job.
Three accounts are special, and do not require a password:
* LocalSystem - The default if no user is specified. Highly privileged.
* LocalService - Very few privileges, lowest security risk.
Apparently not enough privileges for running PowerShell. Do not use.
* NetworkService - Idem, plus network access. Same problems as LocalService.
.PARAMETER Password
Password for UserName. If not specified, you will be prompted for it.
It is strongly recommended NOT to use that argument, as that password is
visible on the console, and in the task manager list.
Instead, use the -UserName argument alone, and wait for the prompt;
or, even better, use the -Credential argument.
.PARAMETER Remove
Uninstall the service.
.PARAMETER Service
Run the service in the background. Used internally by the script.
Do not use, except for test purposes.
.PARAMETER SCMStart
Process Service Control Manager start requests. Used internally by the script.
Do not use, except for test purposes.
.PARAMETER SCMStop
Process Service Control Manager stop requests. Used internally by the script.
Do not use, except for test purposes.
.PARAMETER Control
Send a control message to the service thread.
.PARAMETER Version
Display this script version and exit.
.EXAMPLE
# Setup the service and run it for the first time
C:\PS>.\PSService.ps1 -Status
Not installed
C:\PS>.\PSService.ps1 -Setup
C:\PS># At this stage, a copy of PSService.ps1 is present in the path
C:\PS>PSService -Status
Stopped
C:\PS>PSService -Start
C:\PS>PSService -Status
Running
C:\PS># Load the log file in Notepad.exe for review
C:\PS>notepad ${ENV:windir}\Logs\PSService.log
.EXAMPLE
# Stop the service and uninstall it.
C:\PS>PSService -Stop
C:\PS>PSService -Status
Stopped
C:\PS>PSService -Remove
C:\PS># At this stage, no copy of PSService.ps1 is present in the path anymore
C:\PS>.\PSService.ps1 -Status
Not installed
.EXAMPLE
# Configure the service to run as a different user
C:\PS>$cred = Get-Credential -UserName LAB\Assistant
C:\PS>.\PSService -Setup -Credential $cred
.EXAMPLE
# Send a control message to the service, and verify that it received it.
C:\PS>PSService -Control Hello
C:\PS>Notepad C:\Windows\Logs\PSService.log
# The last lines should contain a trace of the reception of this Hello message
#>
[CmdletBinding(DefaultParameterSetName='Status')]
Param(
[Parameter(ParameterSetName='Start', Mandatory=$true)]
[Switch]$Start, # Start the service
[Parameter(ParameterSetName='Stop', Mandatory=$true)]
[Switch]$Stop, # Stop the service
[Parameter(ParameterSetName='Restart', Mandatory=$true)]
[Switch]$Restart, # Restart the service
[Parameter(ParameterSetName='Status', Mandatory=$false)]
[Switch]$Status = $($PSCmdlet.ParameterSetName -eq 'Status'), # Get the current service status
[Parameter(ParameterSetName='Setup', Mandatory=$true)]
[Parameter(ParameterSetName='Setup2', Mandatory=$true)]
[Switch]$Setup, # Install the service
[Parameter(ParameterSetName='Setup', Mandatory=$true)]
[String]$UserName, # Set the service to run as this user
[Parameter(ParameterSetName='Setup', Mandatory=$false)]
[String]$Password, # Use this password for the user
[Parameter(ParameterSetName='Setup2', Mandatory=$false)]
[System.Management.Automation.PSCredential]$Credential, # Service account credential
[Parameter(ParameterSetName='Remove', Mandatory=$true)]
[Switch]$Remove, # Uninstall the service
[Parameter(ParameterSetName='Service', Mandatory=$true)]
[Switch]$Service, # Run the service (Internal use only)
[Parameter(ParameterSetName='SCMStart', Mandatory=$true)]
[Switch]$SCMStart, # Process SCM Start requests (Internal use only)
[Parameter(ParameterSetName='SCMStop', Mandatory=$true)]
[Switch]$SCMStop, # Process SCM Stop requests (Internal use only)
[Parameter(ParameterSetName='Control', Mandatory=$true)]
[String]$Control = $null, # Control message to send to the service
[Parameter(ParameterSetName='Version', Mandatory=$true)]
[Switch]$Version # Get this script version
)
$scriptVersion = "2017-12-10"
# This script name, with various levels of details
$argv0 = Get-Item $MyInvocation.MyCommand.Definition
$script = $argv0.basename # Ex: PSService
$scriptName = $argv0.name # Ex: PSService.ps1
$scriptFullName = $argv0.fullname # Ex: C:\Temp\PSService.ps1
# Global settings
$serviceName = $script # A one-word name used for net start commands
$serviceDisplayName = "A Sample PowerShell Service"
$ServiceDescription = "Shows how a service can be written in PowerShell"
$pipeName = "Service_$serviceName" # Named pipe name. Used for sending messages to the service task
# $installDir = "${ENV:ProgramFiles}\$serviceName" # Where to install the service files
$installDir = "${ENV:windir}\System32" # Where to install the service files
$scriptCopy = "$installDir\$scriptName"
$exeName = "$serviceName.exe"
$exeFullName = "$installDir\$exeName"
$logDir = "${ENV:windir}\Logs" # Where to log the service messages
$logFile = "$logDir\$serviceName.log"
$logName = "Application" # Event Log name (Unrelated to the logFile!)
# Note: The current implementation only supports "classic" (ie. XP-compatble) event logs.
# To support new style (Vista and later) "Applications and Services Logs" folder trees, it would
# be necessary to use the new *WinEvent commands instead of the XP-compatible *EventLog commands.
# Gotcha: If you change $logName to "NEWLOGNAME", make sure that the registry key below does not exist:
# HKLM\System\CurrentControlSet\services\eventlog\Application\NEWLOGNAME
# Else, New-EventLog will fail, saying the log NEWLOGNAME is already registered as a source,
# even though "Get-WinEvent -ListLog NEWLOGNAME" says this log does not exist!
# If the -Version switch is specified, display the script version and exit.
if ($Version) {
Write-Output $scriptVersion
return
}
#-----------------------------------------------------------------------------#
# #
# Function Now #
# #
# Description Get a string with the current time. #
# #
# Notes The output string is in the ISO 8601 format, except for #
# a space instead of a T between the date and time, to #
# improve the readability. #
# #
# History #
# 2015-06-11 JFL Created this routine. #
# #
#-----------------------------------------------------------------------------#
Function Now {
Param (
[Switch]$ms, # Append milliseconds
[Switch]$ns # Append nanoseconds
)
$Date = Get-Date
$now = ""
$now += "{0:0000}-{1:00}-{2:00} " -f $Date.Year, $Date.Month, $Date.Day
$now += "{0:00}:{1:00}:{2:00}" -f $Date.Hour, $Date.Minute, $Date.Second
$nsSuffix = ""
if ($ns) {
if ("$($Date.TimeOfDay)" -match "\.\d\d\d\d\d\d") {
$now += $matches[0]
$ms = $false
} else {
$ms = $true
$nsSuffix = "000"
}
}
if ($ms) {
$now += ".{0:000}$nsSuffix" -f $Date.MilliSecond
}
return $now
}
#-----------------------------------------------------------------------------#
# #
# Function Log #
# #
# Description Log a string into the PSService.log file #
# #
# Arguments A string #
# #
# Notes Prefixes the string with a timestamp and the user name. #
# (Except if the string is empty: Then output a blank line.)#
# #
# History #
# 2016-06-05 JFL Also prepend the Process ID. #
# 2016-06-08 JFL Allow outputing blank lines. #
# #
#-----------------------------------------------------------------------------#
Function Log () {
Param(
[Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
[String]$string
)
if (!(Test-Path $logDir)) {
New-Item -ItemType directory -Path $logDir | Out-Null
}
if ($String.length) {
$string = "$(Now) $pid $currentUserName $string"
}
$string | Out-File -Encoding ASCII -Append "$logFile"
}
#-----------------------------------------------------------------------------#
# #
# Function Start-PSThread #
# #
# Description Start a new PowerShell thread #
# #
# Arguments See the Param() block #
# #
# Notes Returns a thread description object. #
# The completion can be tested in $_.Handle.IsCompleted #
# Alternative: Use a thread completion event. #
# #
# References #
# https://learn-powershell.net/tag/runspace/ #
# https://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/
# http://www.codeproject.com/Tips/895840/Multi-Threaded-PowerShell-Cookbook
# #
# History #
# 2016-06-08 JFL Created this function #
# #
#-----------------------------------------------------------------------------#
$PSThreadCount = 0 # Counter of PSThread IDs generated so far
$PSThreadList = @{} # Existing PSThreads indexed by Id
Function Get-PSThread () {
Param(
[Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
[int[]]$Id = $PSThreadList.Keys # List of thread IDs
)
$Id | % { $PSThreadList.$_ }
}
Function Start-PSThread () {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ScriptBlock]$ScriptBlock, # The script block to run in a new thread
[Parameter(Mandatory=$false)]
[String]$Name = "", # Optional thread name. Default: "PSThread$Id"
[Parameter(Mandatory=$false)]
[String]$Event = "", # Optional thread completion event name. Default: None
[Parameter(Mandatory=$false)]
[Hashtable]$Variables = @{}, # Optional variables to copy into the script context.
[Parameter(Mandatory=$false)]
[String[]]$Functions = @(), # Optional functions to copy into the script context.
[Parameter(Mandatory=$false)]
[Object[]]$Arguments = @() # Optional arguments to pass to the script.
)
$Id = $script:PSThreadCount
$script:PSThreadCount += 1
if (!$Name.Length) {
$Name = "PSThread$Id"
}
$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
foreach ($VarName in $Variables.Keys) { # Copy the specified variables into the script initial context
$value = $Variables.$VarName
Write-Debug "Adding variable $VarName=[$($Value.GetType())]$Value"
$var = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry($VarName, $value, "")
$InitialSessionState.Variables.Add($var)
}
foreach ($FuncName in $Functions) { # Copy the specified functions into the script initial context
$Body = Get-Content function:$FuncName
Write-Debug "Adding function $FuncName () {$Body}"
$func = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($FuncName, $Body)
$InitialSessionState.Commands.Add($func)
}
$RunSpace = [RunspaceFactory]::CreateRunspace($InitialSessionState)
$RunSpace.Open()
$PSPipeline = [powershell]::Create()
$PSPipeline.Runspace = $RunSpace
$PSPipeline.AddScript($ScriptBlock) | Out-Null
$Arguments | % {
Write-Debug "Adding argument [$($_.GetType())]'$_'"
$PSPipeline.AddArgument($_) | Out-Null
}
$Handle = $PSPipeline.BeginInvoke() # Start executing the script
if ($Event.Length) { # Do this after BeginInvoke(), to avoid getting the start event.
Register-ObjectEvent $PSPipeline -EventName InvocationStateChanged -SourceIdentifier $Name -MessageData $Event
}
$PSThread = New-Object PSObject -Property @{
Id = $Id
Name = $Name
Event = $Event
RunSpace = $RunSpace
PSPipeline = $PSPipeline
Handle = $Handle
} # Return the thread description variables
$script:PSThreadList[$Id] = $PSThread
$PSThread
}
#-----------------------------------------------------------------------------#
# #
# Function Receive-PSThread #
# #
# Description Get the result of a thread, and optionally clean it up #
# #
# Arguments See the Param() block #
# #
# Notes #
# #
# History #
# 2016-06-08 JFL Created this function #
# #
#-----------------------------------------------------------------------------#
Function Receive-PSThread () {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
[PSObject]$PSThread, # Thread descriptor object
[Parameter(Mandatory=$false)]
[Switch]$AutoRemove # If $True, remove the PSThread object
)
Process {
if ($PSThread.Event -and $AutoRemove) {
Unregister-Event -SourceIdentifier $PSThread.Name
Get-Event -SourceIdentifier $PSThread.Name | Remove-Event # Flush remaining events
}
try {
$PSThread.PSPipeline.EndInvoke($PSThread.Handle) # Output the thread pipeline output
} catch {
$_ # Output the thread pipeline error
}
if ($AutoRemove) {
$PSThread.RunSpace.Close()
$PSThread.PSPipeline.Dispose()
$PSThreadList.Remove($PSThread.Id)
}
}
}
Function Remove-PSThread () {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
[PSObject]$PSThread # Thread descriptor object
)
Process {
$_ | Receive-PSThread -AutoRemove | Out-Null
}
}
#-----------------------------------------------------------------------------#
# #
# Function Send-PipeMessage #
# #
# Description Send a message to a named pipe #
# #
# Arguments See the Param() block #
# #
# Notes #
# #
# History #
# 2016-05-25 JFL Created this function #
# #
#-----------------------------------------------------------------------------#
Function Send-PipeMessage () {
Param(
[Parameter(Mandatory=$true)]
[String]$PipeName, # Named pipe name
[Parameter(Mandatory=$true)]
[String]$Message # Message string
)
$PipeDir = [System.IO.Pipes.PipeDirection]::Out
$PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous
$pipe = $null # Named pipe stream
$sw = $null # Stream Writer
try {
$pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, $PipeDir, $PipeOpt)
$sw = new-object System.IO.StreamWriter($pipe)
$pipe.Connect(1000)
if (!$pipe.IsConnected) {
throw "Failed to connect client to pipe $pipeName"
}
$sw.AutoFlush = $true
$sw.WriteLine($Message)
} catch {
Log "Error sending pipe $pipeName message: $_"
} finally {
if ($sw) {
$sw.Dispose() # Release resources
$sw = $null # Force the PowerShell garbage collector to delete the .net object
}
if ($pipe) {
$pipe.Dispose() # Release resources
$pipe = $null # Force the PowerShell garbage collector to delete the .net object
}
}
}
#-----------------------------------------------------------------------------#
# #
# Function Receive-PipeMessage #
# #
# Description Wait for a message from a named pipe #
# #
# Arguments See the Param() block #
# #
# Notes I tried keeping the pipe open between client connections, #
# but for some reason everytime the client closes his end #
# of the pipe, this closes the server end as well. #
# Any solution on how to fix this would make the code #
# more efficient. #
# #
# History #
# 2016-05-25 JFL Created this function #
# #
#-----------------------------------------------------------------------------#
Function Receive-PipeMessage () {
Param(
[Parameter(Mandatory=$true)]
[String]$PipeName # Named pipe name
)
$PipeDir = [System.IO.Pipes.PipeDirection]::In
$PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous
$PipeMode = [System.IO.Pipes.PipeTransmissionMode]::Message
try {
$pipe = $null # Named pipe stream
$pipe = New-Object system.IO.Pipes.NamedPipeServerStream($PipeName, $PipeDir, 1, $PipeMode, $PipeOpt)
$sr = $null # Stream Reader
$sr = new-object System.IO.StreamReader($pipe)
$pipe.WaitForConnection()
$Message = $sr.Readline()
$Message
} catch {
Log "Error receiving pipe message: $_"
} finally {
if ($sr) {
$sr.Dispose() # Release resources
$sr = $null # Force the PowerShell garbage collector to delete the .net object
}
if ($pipe) {
$pipe.Dispose() # Release resources
$pipe = $null # Force the PowerShell garbage collector to delete the .net object
}
}
}
#-----------------------------------------------------------------------------#
# #
# Function Start-PipeHandlerThread #
# #
# Description Start a new thread waiting for control messages on a pipe #
# #
# Arguments See the Param() block #
# #
# Notes The pipe handler script uses function Receive-PipeMessage.#
# This function must be copied into the thread context. #
# #
# The other functions and variables copied into that thread #
# context are not strictly necessary, but are useful for #
# debugging possible issues. #
# #
# History #
# 2016-06-07 JFL Created this function #
# #
#-----------------------------------------------------------------------------#
$pipeThreadName = "Control Pipe Handler"
Function Start-PipeHandlerThread () {
Param(
[Parameter(Mandatory=$true)]
[String]$pipeName, # Named pipe name
[Parameter(Mandatory=$false)]
[String]$Event = "ControlMessage" # Event message
)
Start-PSThread -Variables @{ # Copy variables required by function Log() into the thread context
logDir = $logDir
logFile = $logFile
currentUserName = $currentUserName
} -Functions Now, Log, Receive-PipeMessage -ScriptBlock {
Param($pipeName, $pipeThreadName)
try {
Receive-PipeMessage "$pipeName" # Blocks the thread until the next message is received from the pipe
} catch {
Log "$pipeThreadName # Error: $_"
throw $_ # Push the error back to the main thread
}
} -Name $pipeThreadName -Event $Event -Arguments $pipeName, $pipeThreadName
}
#-----------------------------------------------------------------------------#
# #
# Function Receive-PipeHandlerThread #
# #
# Description Get what the pipe handler thread received #
# #
# Arguments See the Param() block #
# #
# Notes #
# #
# History #
# 2016-06-07 JFL Created this function #
# #
#-----------------------------------------------------------------------------#
Function Receive-PipeHandlerThread () {
Param(
[Parameter(Mandatory=$true)]
[PSObject]$pipeThread # Thread descriptor
)
Receive-PSThread -PSThread $pipeThread -AutoRemove
}
#-----------------------------------------------------------------------------#
# #
# Function $source #
# #
# Description C# source of the PSService.exe stub #
# #
# Arguments #
# #
# Notes The lines commented with "SET STATUS" and "EVENT LOG" are #
# optional. (Or blocks between "// SET STATUS [" and #
# "// SET STATUS ]" comments.) #
# SET STATUS lines are useful only for services with a long #
# startup time. #
# EVENT LOG lines are useful for debugging the service. #
# #
# History #
# 2017-10-04 RBL Updated the OnStop() procedure adding the sections #
# try{ #
# }catch{ #
# }finally{ #
# } #
# This resolved the issue where stopping the service would #
# leave the PowerShell process -Service still running. This #
# unclosed process was an orphaned process that would #
# remain until the pid was manually killed or the computer #
# was rebooted #
# #
#-----------------------------------------------------------------------------#
$scriptCopyCname = $scriptCopy -replace "\\", "\\" # Double backslashes. (The first \\ is a regexp with \ escaped; The second is a plain string.)
$source = @"
using System;
using System.ServiceProcess;
using System.Diagnostics;
using System.Runtime.InteropServices; // SET STATUS
using System.ComponentModel; // SET STATUS
public enum ServiceType : int { // SET STATUS [
SERVICE_WIN32_OWN_PROCESS = 0x00000010,
SERVICE_WIN32_SHARE_PROCESS = 0x00000020,
}; // SET STATUS ]
public enum ServiceState : int { // SET STATUS [
SERVICE_STOPPED = 0x00000001,
SERVICE_START_PENDING = 0x00000002,
SERVICE_STOP_PENDING = 0x00000003,
SERVICE_RUNNING = 0x00000004,
SERVICE_CONTINUE_PENDING = 0x00000005,
SERVICE_PAUSE_PENDING = 0x00000006,
SERVICE_PAUSED = 0x00000007,
}; // SET STATUS ]
[StructLayout(LayoutKind.Sequential)] // SET STATUS [
public struct ServiceStatus {
public ServiceType dwServiceType;
public ServiceState dwCurrentState;
public int dwControlsAccepted;
public int dwWin32ExitCode;
public int dwServiceSpecificExitCode;
public int dwCheckPoint;
public int dwWaitHint;
}; // SET STATUS ]
public enum Win32Error : int { // WIN32 errors that we may need to use
NO_ERROR = 0,
ERROR_APP_INIT_FAILURE = 575,
ERROR_FATAL_APP_EXIT = 713,
ERROR_SERVICE_NOT_ACTIVE = 1062,
ERROR_EXCEPTION_IN_SERVICE = 1064,
ERROR_SERVICE_SPECIFIC_ERROR = 1066,
ERROR_PROCESS_ABORTED = 1067,
};
public class Service_$serviceName : ServiceBase { // $serviceName may begin with a digit; The class name must begin with a letter
private System.Diagnostics.EventLog eventLog; // EVENT LOG
private ServiceStatus serviceStatus; // SET STATUS
public Service_$serviceName() {
ServiceName = "$serviceName";
CanStop = true;
CanPauseAndContinue = false;
AutoLog = true;
eventLog = new System.Diagnostics.EventLog(); // EVENT LOG [
if (!System.Diagnostics.EventLog.SourceExists(ServiceName)) {
System.Diagnostics.EventLog.CreateEventSource(ServiceName, "$logName");
}
eventLog.Source = ServiceName;
eventLog.Log = "$logName"; // EVENT LOG ]
EventLog.WriteEntry(ServiceName, "$exeName $serviceName()"); // EVENT LOG
}
[DllImport("advapi32.dll", SetLastError=true)] // SET STATUS
private static extern bool SetServiceStatus(IntPtr handle, ref ServiceStatus serviceStatus);
protected override void OnStart(string [] args) {
EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Entry. Starting script '$scriptCopyCname' -SCMStart"); // EVENT LOG
// Set the service state to Start Pending. // SET STATUS [
// Only useful if the startup time is long. Not really necessary here for a 2s startup time.
serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS;
serviceStatus.dwCurrentState = ServiceState.SERVICE_START_PENDING;
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ]
// Start a child process with another copy of this script
try {
Process p = new Process();
// Redirect the output stream of the child process.
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = "PowerShell.exe";
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStart"; // Works if path has spaces, but not if it contains ' quotes.
p.Start();
// Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
string output = p.StandardOutput.ReadToEnd();
// Wait for the completion of the script startup code, that launches the -Service instance
p.WaitForExit();
if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
// Success. Set the service state to Running. // SET STATUS
serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS
} catch (Exception e) {
EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Failed to start $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG
// Change the service state back to Stopped. // SET STATUS [
serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED;
Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code
if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is...
w32ex = e.InnerException as Win32Exception;
}
if (w32ex != null) { // Report the actual WIN32 error
serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode;
} else { // Make up a reasonable reason
serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE);
} // SET STATUS ]
} finally {
serviceStatus.dwWaitHint = 0; // SET STATUS
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Exit"); // EVENT LOG
}
}
protected override void OnStop() {
EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Entry"); // EVENT LOG
// Start a child process with another copy of ourselves
try {
Process p = new Process();
// Redirect the output stream of the child process.
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = "PowerShell.exe";
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStop"; // Works if path has spaces, but not if it contains ' quotes.
p.Start();
// Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
string output = p.StandardOutput.ReadToEnd();
// Wait for the PowerShell script to be fully stopped.
p.WaitForExit();
if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
// Success. Set the service state to Stopped. // SET STATUS
serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; // SET STATUS
} catch (Exception e) {
EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Failed to stop $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG
// Change the service state back to Started. // SET STATUS [
serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING;
Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code
if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is...
w32ex = e.InnerException as Win32Exception;
}
if (w32ex != null) { // Report the actual WIN32 error
serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode;
} else { // Make up a reasonable reason
serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE);
} // SET STATUS ]
} finally {
serviceStatus.dwWaitHint = 0; // SET STATUS
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Exit"); // EVENT LOG
}
}
public static void Main() {
System.ServiceProcess.ServiceBase.Run(new Service_$serviceName());
}
}
"@
#-----------------------------------------------------------------------------#
# #
# Function Main #
# #
# Description Execute the specified actions #
# #
# Arguments See the Param() block at the top of this script #
# #
# Notes #
# #
# History #
# #
#-----------------------------------------------------------------------------#
# Identify the user name. We use that for logging.
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$currentUserName = $identity.Name # Ex: "NT AUTHORITY\SYSTEM" or "Domain\Administrator"
if ($Setup) {Log ""} # Insert one blank line to separate test sessions logs
Log $MyInvocation.Line # The exact command line that was used to start us
# The following commands write to the event log, but we need to make sure the PSService source is defined.
New-EventLog -LogName $logName -Source $serviceName -ea SilentlyContinue
# Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block
$Status = ($PSCmdlet.ParameterSetName -eq 'Status')
if ($SCMStart) { # The SCM tells us to start the service
# Do whatever is necessary to start the service script instance
Log "$scriptName -SCMStart: Starting script '$scriptFullName' -Service"
Write-EventLog -LogName $logName -Source $serviceName -EventId 1001 -EntryType Information -Message "$scriptName -SCMStart: Starting script '$scriptFullName' -Service"
Start-Process PowerShell.exe -ArgumentList ("-c & '$scriptFullName' -Service")
return
}
if ($Start) { # The user tells us to start the service
Write-Verbose "Starting service $serviceName"
Write-EventLog -LogName $logName -Source $serviceName -EventId 1002 -EntryType Information -Message "$scriptName -Start: Starting service $serviceName"
Start-Service $serviceName # Ask Service Control Manager to start it
return
}
if ($SCMStop) { # The SCM tells us to stop the service
# Do whatever is necessary to stop the service script instance
Write-EventLog -LogName $logName -Source $serviceName -EventId 1003 -EntryType Information -Message "$scriptName -SCMStop: Stopping script $scriptName -Service"
Log "$scriptName -SCMStop: Stopping script $scriptName -Service"
# Send an exit message to the service instance
Send-PipeMessage $pipeName "exit"
return
}
if ($Stop) { # The user tells us to stop the service
Write-Verbose "Stopping service $serviceName"
Write-EventLog -LogName $logName -Source $serviceName -EventId 1004 -EntryType Information -Message "$scriptName -Stop: Stopping service $serviceName"
Stop-Service $serviceName # Ask Service Control Manager to stop it
return
}
if ($Restart) { # Restart the service
& $scriptFullName -Stop
& $scriptFullName -Start
return
}
if ($Status) { # Get the current service status
$spid = $null
$processes = @(Get-WmiObject Win32_Process -filter "Name = 'powershell.exe'" | Where-Object {
$_.CommandLine -match ".*$scriptCopyCname.*-Service"
})
foreach ($process in $processes) { # There should be just one, but be prepared for surprises.
$spid = $process.ProcessId
Write-Verbose "$serviceName Process ID = $spid"
}
# if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\services\$serviceName") {}
try {
$pss = Get-Service $serviceName -ea stop # Will error-out if not installed
} catch {
"Not Installed"
return
}
$pss.Status
if (($pss.Status -eq "Running") -and (!$spid)) { # This happened during the debugging phase
Write-Error "The Service Control Manager thinks $serviceName is started, but $serviceName.ps1 -Service is not running."
exit 1
}
return
}
if ($Setup) { # Install the service
# Check if it's necessary
try {
$pss = Get-Service $serviceName -ea stop # Will error-out if not installed
# Check if this script is newer than the installed copy.
if ((Get-Item $scriptCopy -ea SilentlyContinue).LastWriteTime -lt (Get-Item $scriptFullName -ea SilentlyContinue).LastWriteTime) {
Write-Verbose "Service $serviceName is already Installed, but requires upgrade"
& $scriptFullName -Remove
throw "continue"
} else {
Write-Verbose "Service $serviceName is already Installed, and up-to-date"
}
exit 0
} catch {
# This is the normal case here. Do not throw or write any error!
Write-Debug "Installation is necessary" # Also avoids a ScriptAnalyzer warning
# And continue with the installation.
}
if (!(Test-Path $installDir)) {
New-Item -ItemType directory -Path $installDir | Out-Null
}
# Copy the service script into the installation directory
if ($ScriptFullName -ne $scriptCopy) {
Write-Verbose "Installing $scriptCopy"
Copy-Item $ScriptFullName $scriptCopy
}
# Generate the service .EXE from the C# source embedded in this script
try {
Write-Verbose "Compiling $exeFullName"
Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false
} catch {
$msg = $_.Exception.Message
Write-error "Failed to create the $exeFullName service stub. $msg"
exit 1
}
# Register the service
Write-Verbose "Registering service $serviceName"
if ($UserName -and !$Credential.UserName) {
$emptyPassword = New-Object -Type System.Security.SecureString
switch ($UserName) {
{"LocalService", "NetworkService" -contains $_} {
$Credential = New-Object -Type System.Management.Automation.PSCredential ("NT AUTHORITY\$UserName", $emptyPassword)
}
{"LocalSystem", ".\LocalSystem", "${env:COMPUTERNAME}\LocalSystem", "NT AUTHORITY\LocalService", "NT AUTHORITY\NetworkService" -contains $_} {
$Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $emptyPassword)
}
default {
if (!$Password) {
$Credential = Get-Credential -UserName $UserName -Message "Please enter the password for the service user"
} else {
$securePassword = ConvertTo-SecureString $Password -AsPlainText -Force
$Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $securePassword)
}
}
}
}
if ($Credential.UserName) {
Log "$scriptName -Setup # Configuring the service to run as $($Credential.UserName)"
$pss = New-Service $serviceName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -Credential $Credential
} else {
Log "$scriptName -Setup # Configuring the service to run by default as LocalSystem"
$pss = New-Service $serviceName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic
}
return
}
if ($Remove) { # Uninstall the service
# Check if it's necessary
try {
$pss = Get-Service $serviceName -ea stop # Will error-out if not installed
} catch {
Write-Verbose "Already uninstalled"
return
}
Stop-Service $serviceName # Make sure it's stopped
# In the absence of a Remove-Service applet, use sc.exe instead.
Write-Verbose "Removing service $serviceName"
$msg = sc.exe delete $serviceName
if ($LastExitCode) {
Write-Error "Failed to remove the service ${serviceName}: $msg"
exit 1
} else {
Write-Verbose $msg
}
# Remove the installed files
if (Test-Path $installDir) {
foreach ($ext in ("exe", "pdb", "ps1")) {
$file = "$installDir\$serviceName.$ext"
if (Test-Path $file) {
Write-Verbose "Deleting file $file"
Remove-Item $file
}
}
if (!(@(Get-ChildItem $installDir -ea SilentlyContinue)).Count) {
Write-Verbose "Removing directory $installDir"
Remove-Item $installDir
}
}
Log "" # Insert one blank line to separate test sessions logs
return
}
if ($Control) { # Send a control message to the service
Send-PipeMessage $pipeName $control
}
if ($Service) { # Run the service
Write-EventLog -LogName $logName -Source $serviceName -EventId 1005 -EntryType Information -Message "$scriptName -Service # Beginning background job"
# Do the service background job
try {
# Start the control pipe handler thread
$pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage"
######### TO DO: Implement your own service code here. ##########
###### Example that wakes up and logs a line every 10 sec: ######
# Start a periodic timer
$timerName = "Sample service timer"
$period = 10 # seconds
$timer = new-object System.Timers.Timer
$timer.Interval = ($period * 1000) # Milliseconds
$timer.AutoReset = $true # Make it fire repeatedly
Register-ObjectEvent $timer -EventName Elapsed -SourceIdentifier $timerName -MessageData "TimerTick"
$timer.start() # Must be stopped in the finally block
# Now enter the main service event loop
do { # Keep running until told to exit by the -Stop handler
$event = Wait-Event # Wait for the next incoming event
$source = $event.SourceIdentifier
$message = $event.MessageData
$eventTime = $event.TimeGenerated.TimeofDay
Write-Debug "Event at $eventTime from ${source}: $message"
$event | Remove-Event # Flush the event from the queue
switch ($message) {
"ControlMessage" { # Required. Message received by the control pipe thread
$state = $event.SourceEventArgs.InvocationStateInfo.state
Write-Debug "$script -Service # Thread $source state changed to $state"
switch ($state) {
"Completed" {
$message = Receive-PipeHandlerThread $pipeThread
Log "$scriptName -Service # Received control message: $Message"
if ($message -ne "exit") { # Start another thread waiting for control messages
$pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage"
}
}
"Failed" {
$error = Receive-PipeHandlerThread $pipeThread
Log "$scriptName -Service # $source thread failed: $error"
Start-Sleep 1 # Avoid getting too many errors
$pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" # Retry
}
}
}
"TimerTick" { # Example. Periodic event generated for this example
Log "$scriptName -Service # Timer ticked"
}
default { # Should not happen
Log "$scriptName -Service # Unexpected event from ${source}: $Message"
}
}
} while ($message -ne "exit")
} catch { # An exception occurred while runnning the service
$msg = $_.Exception.Message
$line = $_.InvocationInfo.ScriptLineNumber
Log "$scriptName -Service # Error at line ${line}: $msg"
} finally { # Invoked in all cases: Exception or normally by -Stop
# Cleanup the periodic timer used in the above example
Unregister-Event -SourceIdentifier $timerName
$timer.stop()
############### End of the service code example. ################
# Terminate the control pipe handler thread
Get-PSThread | Remove-PSThread # Remove all remaining threads
# Flush all leftover events (There may be some that arrived after we exited the while event loop, but before we unregistered the events)
$events = Get-Event | Remove-Event
# Log a termination event, no matter what the cause is.
Write-EventLog -LogName $logName -Source $serviceName -EventId 1006 -EntryType Information -Message "$script -Service # Exiting"
Log "$scriptName -Service # Exiting"
}
return
}
If you need the service to run in Safe Mode, you can add the following registry key/value:
Key: HKLM:\SYSTEM\CurrentControlSet\Control\SafeBoot\Minimal\<ServiceName>
Name: (Default)
String Value: Service
While we’re at it, here are the commands to set the computer to boot into and exit from Safe Mode.
# Enter Safe Mode on boot:
bcdedit /set {current} safeboot minimal
# Enter Normal Mode on boot:
bcdedit /deletevalue {default} safeboot
References: GitHub – JFLarvoire/SysToolsLib: A library of Windows and Linux system management tools
Windows services are a crucial part of the Windows operating system, allowing developers to create long-running executable applications that can start automatically when the system boots up. However, sometimes you may need to run a PowerShell script as a Windows service, which can be a bit tricky. In this article, we will explore the different ways to make a Windows service run a PowerShell script, including using the Windows Task Scheduler, creating a custom Windows service, and leveraging third-party tools.
Understanding Windows Services and PowerShell Scripts
Before we dive into the details of running a PowerShell script as a Windows service, let’s first understand what Windows services and PowerShell scripts are.
Windows services are executable programs that run in the background, providing specific functionality to the system. They can start automatically when the system boots up and can run under a specific user account. Windows services are typically used for tasks that require continuous execution, such as monitoring system resources, providing network services, or running scheduled tasks.
PowerShell scripts, on the other hand, are files that contain a series of PowerShell commands that can be executed in sequence. PowerShell is a powerful task automation and configuration management framework from Microsoft, consisting of a command-line shell and scripting language built on top of the .NET framework. PowerShell scripts can be used to automate a wide range of tasks, from simple file management to complex system administration tasks.
Why Run a PowerShell Script as a Windows Service?
There are several reasons why you may want to run a PowerShell script as a Windows service:
- Automation: Running a PowerShell script as a Windows service allows you to automate tasks that need to run continuously in the background.
- Flexibility: PowerShell scripts can be easily modified and updated, making it easy to change the behavior of the Windows service.
- Integration: PowerShell scripts can integrate with other Windows services and applications, making it easy to automate complex tasks.
Method 1: Using the Windows Task Scheduler
One way to run a PowerShell script as a Windows service is to use the Windows Task Scheduler. The Task Scheduler is a built-in Windows utility that allows you to schedule tasks to run at specific times or intervals.
To run a PowerShell script using the Task Scheduler, follow these steps:
- Open the Task Scheduler: You can open the Task Scheduler by searching for it in the Start menu or by typing “taskschd.msc” in the Run dialog box.
- Create a new task: In the Task Scheduler, click on “Create Basic Task” in the right-hand Actions panel.
- Give the task a name and description: Enter a name and description for the task, and then click “Next”.
- Set the trigger: Set the trigger for the task, such as “Daily” or “At startup”, and then click “Next”.
- Set the action: Set the action for the task, which is to run the PowerShell script. Click “New” and then enter the following:
- Program/script: Enter the path to the PowerShell executable, which is typically “C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe”.
- Add arguments: Enter the path to the PowerShell script, preceded by the “-File” parameter. For example: “-File C:\Scripts\myscript.ps1”
- Click “Next” and then “Finish” to create the task.
The Task Scheduler will now run the PowerShell script at the specified interval.
Method 2: Creating a Custom Windows Service
Another way to run a PowerShell script as a Windows service is to create a custom Windows service. This method requires more effort, but provides more flexibility and control.
To create a custom Windows service, you will need to:
- Create a new C# project: Create a new C# project in Visual Studio, and select the “Windows Service” template.
- Add the PowerShell runtime: Add the PowerShell runtime to the project by installing the “System.Management.Automation” NuGet package.
- Create a new service class: Create a new class that inherits from the “ServiceBase” class, and override the “OnStart” and “OnStop” methods.
- Run the PowerShell script: In the “OnStart” method, use the PowerShell runtime to run the PowerShell script.
Here is an example of what the code might look like:
“`csharp
using System.ServiceProcess;
using System.Management.Automation;
namespace MyWindowsService
{
public class MyService : ServiceBase
{
public MyService()
{
ServiceName = “MyService”;
}
protected override void OnStart(string[] args)
{
using (PowerShell powershell = PowerShell.Create())
{
powershell.AddScript("C:\\Scripts\\myscript.ps1");
powershell.Invoke();
}
}
protected override void OnStop()
{
// Stop the service
}
}
}
“`
To install the service, you will need to use the “InstallUtil” utility, which is part of the .NET Framework.
Method 3: Using Third-Party Tools
There are also several third-party tools available that can help you run a PowerShell script as a Windows service. Some popular options include:
- NSSM: The Non-Sucking Service Manager (NSSM) is a free, open-source service manager that can be used to run any executable, including PowerShell scripts.
- WinSW: WinSW is a free, open-source service wrapper that can be used to run any executable, including PowerShell scripts.
To use one of these tools, simply download and install the tool, and then follow the instructions for creating a new service.
Conclusion
Running a PowerShell script as a Windows service can be a powerful way to automate tasks and integrate with other Windows services and applications. In this article, we explored three different methods for running a PowerShell script as a Windows service: using the Windows Task Scheduler, creating a custom Windows service, and leveraging third-party tools. Each method has its own advantages and disadvantages, and the choice of which method to use will depend on your specific needs and requirements.
By following the steps outlined in this article, you should be able to run a PowerShell script as a Windows service and take advantage of the automation and flexibility that it provides.
Best Practices for Running PowerShell Scripts as Windows Services
Here are some best practices to keep in mind when running PowerShell scripts as Windows services:
- Use a secure account: Make sure to run the service under a secure account that has the necessary permissions to execute the script.
- Use a try-catch block: Use a try-catch block in your PowerShell script to catch any errors that may occur and prevent the service from crashing.
- Log errors: Log any errors that occur to a file or event log, so that you can diagnose and fix any issues that may arise.
- Test thoroughly: Test your PowerShell script thoroughly before deploying it as a Windows service, to ensure that it works as expected.
By following these best practices, you can ensure that your PowerShell script runs reliably and securely as a Windows service.
What are the benefits of running PowerShell scripts as Windows services?
Running PowerShell scripts as Windows services provides several benefits, including the ability to run scripts continuously in the background without requiring user interaction. This allows for automation of tasks that need to run at regular intervals or in response to specific events. Additionally, running scripts as services provides a high level of reliability and fault tolerance, as the script will continue to run even if the user logs off or the system is restarted.
Another benefit of running PowerShell scripts as Windows services is the ability to manage and monitor the script’s execution. Windows services provide a built-in mechanism for managing the lifecycle of the script, including starting, stopping, and restarting the script. This makes it easy to manage and troubleshoot the script’s execution, and to ensure that it is running as expected.
What are the requirements for running a PowerShell script as a Windows service?
To run a PowerShell script as a Windows service, you need to have Windows PowerShell 3.0 or later installed on your system. Additionally, you need to have the necessary permissions to create and manage Windows services. This typically requires administrative privileges on the system. You also need to have a PowerShell script that is designed to run as a service, which means it should be able to run continuously in the background without requiring user interaction.
You also need to have a service wrapper or a third-party tool that can host the PowerShell script as a Windows service. This is because PowerShell scripts cannot be run directly as Windows services. The service wrapper or third-party tool provides the necessary infrastructure to host the script and manage its execution as a Windows service.
How do I create a Windows service to run a PowerShell script?
To create a Windows service to run a PowerShell script, you need to use a service wrapper or a third-party tool that can host the script as a service. One popular option is the Windows Service Wrapper (NSSM), which is a free and open-source tool that can be used to create Windows services. You can download and install NSSM on your system, and then use it to create a new Windows service that runs your PowerShell script.
To create the service, you need to provide the path to the PowerShell executable, the path to the script, and any other necessary parameters. You also need to specify the service name, description, and other settings as required. Once you have created the service, you can install it on your system and manage its execution using the Windows Services console.
How do I install and manage a Windows service that runs a PowerShell script?
To install a Windows service that runs a PowerShell script, you need to use the Windows Services console or the sc.exe command-line tool. You can use the New-Service cmdlet in PowerShell to create a new service, or you can use the NSSM tool to install the service. Once the service is installed, you can manage its execution using the Windows Services console or the Get-Service and Start-Service cmdlets in PowerShell.
To manage the service, you can use the Windows Services console to start, stop, and restart the service. You can also use the Get-Service cmdlet to retrieve information about the service, and the Set-Service cmdlet to modify the service’s settings. Additionally, you can use the Event Viewer to monitor the service’s execution and troubleshoot any issues that may arise.
How do I troubleshoot issues with a Windows service that runs a PowerShell script?
To troubleshoot issues with a Windows service that runs a PowerShell script, you can use the Event Viewer to monitor the service’s execution and retrieve error messages. You can also use the Get-Service cmdlet to retrieve information about the service, and the Get-Process cmdlet to retrieve information about the PowerShell process that is running the script.
Additionally, you can use the PowerShell console to debug the script and identify any issues that may be causing the service to fail. You can also use the -Verbose and -Debug parameters with the PowerShell cmdlets to retrieve detailed information about the script’s execution. By using these tools and techniques, you can troubleshoot issues with the service and ensure that it is running as expected.
Can I use a Windows service to run a PowerShell script that requires user interaction?
No, you cannot use a Windows service to run a PowerShell script that requires user interaction. Windows services are designed to run in the background without requiring user interaction, and they do not have the ability to display a user interface or prompt the user for input. If your script requires user interaction, you will need to run it in a different context, such as in the PowerShell console or in a Windows Forms application.
However, you can use a Windows service to run a PowerShell script that requires input from a file or a database. In this case, the script can read the input from the file or database and process it without requiring user interaction. You can also use a Windows service to run a PowerShell script that generates output to a file or a database, which can then be reviewed by the user.
Are there any security considerations when running a PowerShell script as a Windows service?
Yes, there are several security considerations when running a PowerShell script as a Windows service. One consideration is the account under which the service runs, which should be a least-privilege account that has only the necessary permissions to execute the script. You should also ensure that the script is signed with a digital certificate to prevent tampering and ensure its authenticity.
Additionally, you should ensure that the script does not contain any sensitive information, such as passwords or encryption keys, which could be compromised if the script is accessed by an unauthorized user. You should also use secure protocols for any network communication, such as HTTPS or SSH, to prevent eavesdropping and tampering. By following these security considerations, you can ensure that your PowerShell script is running securely as a Windows service.
Don’t let AI Agents fail in production
Restack backend framework provides long-running workflows and infrastructure for reliable & accurate AI agents.
Get started with example agents
Research Paper
Agent accuracy benchmark
Many enterprises are exploring AI agents, but one issue blocks their adoption: keeping them accurate and on brand. General-purpose LLMs hit only 51% accuracy, while fine-tuned small agents reach 99.7%.
The trust in AI is eroding due to unreliable, poorly designed agents. For AI to reach its full potential, we need better ones. Restack helps you build agents that are reliable, scalable, and ready for real-world use.
Features
The Restack framework
Build reliable and accurate AI agents with Restack.
Developer UI
Simulate, time travel and replay AI agents
The Restack developer toolkit provides a UI to visualize and replay workflows or individual steps. Open a favourite IDE like VS Code or Cursor on one side and view workflows on the other to improve debugging and local development.
Get started in seconds
Start building with Restack AI framework and deploy with Restack Cloud.
Red or blue pill
If you are in the same rabbit-hole as I was of setting up a Windows Service of any form of looping script, there’s two pills you can choose from:
-
Red Pill: Create a program that abide to the law of the fearsome Service Control Manager.
-
Blue Pill: Write a PowerShell script, 8 lines of XML, and download WinSW.exe
WinSW describes itself as following:
A wrapper executable that can run any executable as a Windows service, in a permissive license.
Naturally as someone who enjoys coding with hand grenades, I took the Blue Pill and here’s how that story went:
The Blue Pill
- Create a new working directory and save it to a variable
$DirParams = @{
ItemType = 'Directory'
Name = "PowerShell_Service"
OutVariable = 'WorkingDirectory'
}
New-Item @DirParams
- Download the latest WinSW-x64.exe to the working directory
# Get the latest WinSW 64-bit executable browser download url
$ExecutableName = 'WinSW-x64.exe'
$LatestURL = Invoke-RestMethod 'https://api.github.com/repos/winsw/winsw/releases/latest'
$LatestDownloadURL = ($LatestURL.assets | Where-Object {$_.Name -eq $ExecutableName}).browser_download_url
$FinalPath = "$($WorkingDirectory.FullName)\$ExecutableName"
# Download it to the newly created working directory
Invoke-WebRequest -Uri $LatestDownloadURL -Outfile $FinalPath -Verbose
- Create the PowerShell script which the service runs
This loop checks for notepad every 5 sec and kills it if it finds it
while ($true) {
$notepad = Get-Process notepad -ErrorAction SilentlyContinue
if ($notepad) {
$notepad.Kill()
}
Start-Sleep -Seconds 5
}
- Construct the .XML file
Just edit the id
, name
, description
and startarguments
<service>
<id>PowerShellService</id>
<name>PowerShellService</name>
<description>This service runs a custom PowerShell script.</description>
<executable>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</executable>
<startarguments>-NoLogo -file C:\Path\To\Script\Invoke-PowerShellServiceScript.ps1</startarguments>
<log mode="roll"></log>
</service>
Save the .xml, in this example I saved it as PowerShell_Service.xml
# if not already, step into the workingdirectory
cd $WorkingDirectory.FullName
# Install the service
.\WinSW-x64.exe install .\PowerShell_Service.xml
# Make sure powershell.exe's executionpolicy is Bypass
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope LocalMachine
# As an administrator
Get-Service PowerShellService | Start-Service
Conclusion
Running a PowerShell script as a service on any windows machine isn’t that complicated thanks to WinSW. It’s a great choice if you don’t want to get deeper into the process of developing windows services (it’s kind of a fun rabbit-hole though).
I recommend reading docs of WinSW.
Some things to consider:
- The service will run PowerShell 5.1 as System
- Meaning the executionpolicy must be supporting that usecase (bypass as local machine will do)
- The script in this example is just a demo of a loop, but anything you can think of that loops will do here
- Starting the Service requires elevated rights in this example
- If you get the notorious
The service did not respond to the start or control request in a timely fashion
, you have my condolences (This is a very general error msg that has no clear answer by itself it seems)