PowerShell Studio Projects: Windows Service Project

This article is the next installment in our PowerShell Studio Project series and covers the basics of working with Windows Service Projects. Windows Services are programs that run in the background, with no user interaction. Services can start automatically when the system boots or they can start on demand—as requested by applications that rely on them.

The Windows Service Project template allows you to create a Windows service script in a project:

The Windows Service Project utilizes the same predefined functions used by the single file Windows Service Script template. You can use this template as part of a project or as a stand-alone script. This script template is also included in PrimalScript. It’s important to note that you should not rename or omit any of the predefined functions when using this template because they are used when packaging with the SAPIEN Script Packager.

The functions are as follows:

Start-MyService

function Start-MyService
{
# Place one time startup code here.
$global:bRunService = $true
$global:bServiceRunning = $false
$global:bServicePaused = $false
}

This function is called when your service starts. You should place all initialization code for your service here, such as opening connections or initializing global variables.

Invoke-MyService

function Invoke-MyService
{
$global:bServiceRunning = $true
while($global:bRunService) {
try
{
if($global:bServicePaused -eq $false) #Only act if service is not paused
{
#Place code for your service here
        }
    }
    catch
    {
        # Log exception in application log
        Write-Host $_.Exception.Message 
    }
    # Adjust sleep timing to determine how often your service becomes active.
    if($global:bServicePaused -eq $true) 
    {
        Start-Sleep -Seconds 20 # if the service is paused we sleep longer between checks.
    }
    else 
    {
        Start-Sleep –Seconds 10 # a lower number will make your service active more often and use more CPU cycles
    }
}
$global:bServiceRunning    = $false
}

This is your service’s main loop, where the main functionality of your service takes place. This function will run continuously as your service runs, following a process–sleep–process cycle.

Stop-MyService

function Stop-MyService
{
$global:bRunService = $false # Signal main loop to exit
$CountDown = 30 # Maximum wait for loop to exit
while($global:bServiceRunning -and $Countdown -gt 0)
{
Start-Sleep -Seconds 1 # wait for your main loop to exit
$Countdown = $Countdown - 1
}
# Place code to be executed on service stop here
}

This function is called when your service is asked to stop by the operating system or when using the Stop-Service command. You can see some management code in here that will allow your main service loop to exit gracefully. Place code here to close connections and release resources.

It’s important to note that the main service thread is forcefully terminated after you exit the Stop-MyService function. Make sure you terminate running jobs and secondary runspaces here; otherwise the service process may hang and not exit properly.

Because some modules create their own threads, jobs, runspaces, etc., it is also a good idea to use the Remove-Module command for any modules you load implicitly or explicitly. The script itself does not operate as a service, but you could dot source it from a test script to execute the functions for testing.

Pause-MyService

function Pause-MyService
{
# Service is being paused
# Save state
$global:bServicePaused = $true
# Note that the thread your PowerShell script is running on is not suspended on 'pause'.
# It is your responsibility in the service loop to pause processing until a 'continue' command is issued.
}

This function pauses your service’s main loop and suspends your service. This functionality is handled by you in the service loop as part of the Invoke-MyService function; if this is not included, your service will not pause as expected. While your service is paused, the thread your script is running on is not suspended; instead, the service loop is not running. It is a good idea to sleep for longer periods between loop iterations when the service is paused; this helps to prevent excessive CPU usage by simply waiting and looping.

Continue-MyService

function Continue-MyService
{
# Service is being continued from a paused state
# Restore any saved states if needed
$global:bServicePaused = $false
}

This will resume your service’s main loop. Again, this is only applicable if your service loop is utilizing $global:bServicePaused.

When you first create a Windows Service project, a PSPROJ and PSS file are created:

All functions mentioned above reside in Startup.pss. The only project setting on this file that can be changed is the Build Order. This is only important if additional files are added to the project and if the order in which the files are combined matters.

Building and Running a Windows Service Project

When running a project, the project files are built based on their build setting and build order. All files set to include will be combined into one file called ProjectName.Run.ps1.

This script itself does not operate as a service, but you can dot source it from a test script to execute the functions for testing. To make a real Windows service, you need to package your script with a PowerShell Windows Service engine.

Packaging

To package your project with SAPIEN Script Packager, have the project open and open the packager settings from either the Deploy tab (Deploy->Packager) or (Home->Build and Run):

Deploy->Packager
Home->Build and Run

There is a specific engine called SAPIEN PowerShell V5 Host (Windows Service); make sure this is the engine selected under the Script Engine section:

Script Engine

Once the settings have been configured, you can build from either SAPIEN Script Packager or PowerShell Studio.

You will need to install first to run it. To install specify /i when running your executable:

.\WindowsServiceTest.exe /i

This needs to be done with administrator rights.

To uninstall, specify /u when running your executable:

.\WindowsServiceTest.exe /u

Managing the start or stop of your service can be done with the PowerShell service commands or through the Windows Service Control Panel.

For more information on services, please refer to the following:
Windows PowerShell – Writing Windows Services in PowerShell | Microsoft Docs

Installer

Besides the basic information needed for an installer (e.g., Product Name, Company Name), there are some additional steps to have SAPIEN Script Packager install your service.

Under Product Details, the Product Type should be set to Windows Application:

Product Type

Add your packaged Window Service executable along with any other files your service needs to the File and Folders section:

File and Folders

Under the Services section, you must specify the service to install. To do this, you need to add a new Service under Service to install:

New Service

Then add the service executable for the Service File:

Service File

This can be typed manually or selected from the Select Installed File dialog:

A Description for the service will also need be needed:

Description

Once the settings have been configured, you can build from either SAPIEN Script Packager or PowerShell Studio.

When using an installer with a service, the service must be stopped in order to be uninstalled or updated. If the stopping of the service takes too long, Windows Installer will fail based on the locked process, eventually resulting in a time out with an error. This timeout error can also happen when manually attempting to stop a service in the Windows Service Control Panel or using the command Stop-Service. The service can still stop itself after that; it’s just that the wait times out.

Related

Feedback

All information provided in this article is based on the functionality available in PowerShell Studio 2022 build 5.8.207.

As always, any feedback is appreciated. If you have a particular type of blog article or product feature request you would like to see, please submit your suggestions on the Wish List and Feature Requests forum or the new Feature Requests page.