Releasing Windows Services using Azure DevOps

Deploying Windows Services using pipelines in Azure DevOps

With this new post, I continue to explain how releasing and deploying Windows Services using pipelines in Azure DevOps. This helps you achieve a proper CD/CI for your Windows Service projects. Some context is provided by the Microsoft documentation.

Here the list of posts related to this one:

The source code of this post is available on GitHub.

Windows Services built on .NET Core and classic .NET Framework can be deployed using Azure DevOps to our target machine(s). They can automatically run on these machines. This process removes the need to copy files manually.

You will have the following ready:

  • An Azure DevOps account
  • Your working Windows Service code is committed in Azure DevOps Repositories
  • A target Windows machine to deploy to with an internet connection that you have access to. The target machine must have access to the URL dev.azure.com

So, I assume that the code for the Windows Service is in a repository and the pipeline will build the service in this repository and deploy it on the Windows Service machine.

Configuring Windows Service

Before starting with the creation of the Release pipeline, it is important to know sc.exe. This is a tool by Microsoft to manipulate the Windows Services. We can create, stop and remove services for example.

The sc.exe config command is used to modify the configuration of a service in the Windows registry. It also modifies the Service Control Manager database. This command allows you to change various parameters of a service. You can modify its type, start mode, and error control. Additionally, you can adjust the binary path, dependencies, and more.

The basic syntax of the sc.exe config command is as follows:

sc.exe [<servername>] config [<servicename>] 
    [type= {own | share | kernel | filesys | rec | adapt | interact type= {own | share}}] 
    [start= {boot | system | auto | demand | disabled | delayed-auto}] 
    [error= {normal | severe | critical | ignore}] 
    [binpath= <binarypathname>] 
    [group= <loadordergroup>] 
    [tag= {yes | no}] 
    [depend= <dependencies>] 
    [obj= {<accountname> | <objectname>}] 
    [displayname= <displayname>] 
    [password= <password>]

Here the list of parameters in detail:

ParameterDescription
<servername>Specifies the name of the remote server on which the service is located. The name must use the Universal Naming Convention (UNC) format (for example, \myserver). To run SC.exe locally, don’t use this parameter.
<servicename>Specifies the service name returned by the getkeyname operation.
type= {own | share | kernel | filesys | rec | adapt | interact type= {own | share}}Specifies the service type. The options include:
own – Specifies a service that runs in its own process. It doesn’t share an executable file with other services. This is the default value.
share – Specifies a service that runs as a shared process. It shares an executable file with other services.
kernel – Specifies a driver.
filesys – Specifies a file system driver.rec – Specifies a file system-recognized driver that identifies file systems used on the computer.
adapt – Specifies an adapter driver that identifies hardware devices such as keyboards, mice, and disk drives.
interact – Specifies a service that can interact with the desktop, receiving input from users. Interactive services must be run under the LocalSystem account. This type must be used in conjunction with type= own or type= shared (for example, type= interact type= own). Using type= interact by itself will generate an error.
start= {boot | system | auto | demand | disabled | delayed-auto}Specifies the start type for the service. The options include:
boot – Specifies a device driver that is loaded by the boot loader.
system – Specifies a device driver that is started during kernel initialization.
auto – Specifies a service that automatically starts each time the computer is restarted and runs even if no one logs on to the computer.
demand – Specifies a service that must be started manually. This is the default value if start= is not specified.
disabled – Specifies a service that cannot be started. To start a disabled service, change the start type to some other value.
delayed-auto – Specifies a service that starts automatically a short time after other auto services are started.
error= {normal | severe | critical | ignore}Specifies the severity of the error if the service fails to start when the computer is started. The options include:
normal – Specifies that the error is logged and a message box is displayed, informing the user that a service has failed to start. Startup will continue. This is the default setting.
severe – Specifies that the error is logged (if possible). The computer attempts to restart with the last-known good configuration. This could result in the computer being able to restart, but the service may still be unable to run.
critical – Specifies that the error is logged (if possible). The computer attempts to restart with the last-known good configuration. If the last-known good configuration fails, startup also fails, and the boot process halts with a Stop error.
ignore – Specifies that the error is logged and startup continues. No notification is given to the user beyond recording the error in the Event Log.
binpath= <binarypathname>Specifies a path to the service binary file. There is no default for binpath=, and this string must be supplied.

Additionally, ntsd -d can be specified in front of the string for debugging. For more information, see Debugging using CDB and NTSD.
group= <loadordergroup>Specifies the name of the group of which this service is a member. The list of groups is stored in the registry, in the HKLM\System\CurrentControlSet\Control\ServiceGroupOrder subkey. The default value is null.
tag= {yes | no}Specifies whether or not to obtain a TagID from the CreateService call. Tags are used only for boot-start and system-start drivers.
depend= <dependencies>Specifies the names of services or groups that must start before this service. The names are separated by forward slashes (/).
obj= {<accountname> | <objectname>}Specifies a name of an account in which a service will run, or specifies a name of the Windows driver object in which the driver will run. The default setting is LocalSystem.
displayname= <displayname>Specifies a descriptive name for identifying the service in user interface programs. For example, the subkey name of one particular service is wuauserv, which has a more friendly display name of Automatic Updates.
password= <password>Specifies a password. This is required if an account other than the LocalSystem account is used.
/?Displays help at the command prompt.

For more information about sc.exe, read the Microsoft documentation here:

Create the Release pipeline

So, in my previous post, I created the pipeline to build the project and publish it in the artifacts. Now in Azure DevOps, go to Pipelines > Releases > New and then New Release Pipeline.

Create a new Release pipeline - Releasing Windows Services using Azure DevOps
Create a new Release pipeline

On the Template selector that shows up, choose Empty Job at the top.

Create a new Release pipeline - Releasing Windows Services using Azure DevOps
Create a new Release pipeline

Then give this step the name you like. I called mine as the DEV (before it will deploy on the developer machine) and closed the right-hand panel for this. Usually, the name of this stage should be the name of the environment you are deploying to.

Create the Empty job in the Azure DevOps pipeline
Create the Empty job in the Azure DevOps pipeline

Next, click on Add an artifact and then select the source of your Artifact. In the following screenshot, I select the Build from the project in Azure DevOps. The the source is the pipeline of the project (in my case, the project is called WindowsServiceTest). That is now available from our Azure Artifacts that I built previously.

Select the artifacts from the list
Select the artifacts from the list

Then, click on the Add button. For a CD/CI of this service, make sure to click the Lightning icon on the Artifact just added. Then Enable Continuous deployment trigger and click Save then Click OK. Doing this will mean that every time you commit code to your project Repository in Azure Repos and a Build completes successfully, a new Release is automatically created and triggered.

Enable the continuous deployment trigger - Releasing Windows Services using Azure DevOps
Enable the continuous deployment trigger

Now, we have to add the tasks for the real deployment. I’m going to split step by step all the tasks to add. From the tabs, click now Tasks. The screen is looking like the following screenshot.

Tasks tab in the pipeline
Tasks tab in the pipeline

Add the tasks

In the Tasks page we are now looking at, first click on Agent Job. Then click on Remove on the right-hand side to remove it.

Remove Agent job
Remove Agent job

Now click on the 3 dots next to ‘DEV’ (or the name of your deployment process) and add a Deployment group job.

Add a deployment group job
Add a deployment group job

Add Deployment group job

After adding a new Deployment group job, a bit of configuration is required. In the settings of this option, select from the dropdown list of Deployment group, the name of the group to use for the deployment. In my case, it will be Deployment machine.

Select the Deployment group
Select the Deployment group

This is how it looks after the selection of the Deployment machine. Now, I want to add a new task. For that, I have to click on the + related to the Deployment group job.

Add a deployment group job
Add a deployment group job

Add Command line task to uninstall the service

Now, the next step is to stop and remove the service if it already exists. I want to save all the Windows Services in a common directory. Each service should have its own directory within it. I called the common folder ADOWindowsServices (ADO = Azure DevOps). Remember that my service is called WindowsServiceTest. So, I want to check as follows:

  • stop the Windows service
  • delete the Windows service
  • verify if the folder for the service exists. If it does not exist, I can skip the steps up to NODIR
  • Enter in the ADOWindowsServices
  • Delete the folder

To stop and delete the service, I use sc.exe

sc STOP <servicename>
sc DELETE <servicename>

I found that this command is working better if the parameters are in capital letters. Maybe it is just me.

Then, I want to check if the folder for the service exists already. The folder that contains all the other folders is located in the root of C:.

C:
IF NOT EXIST C:\ADOWindowsServices\WindowsServiceTest GOTO NODIR

if the folder for the service is not exists, the script will jump to the label NODIR. If not, it opens the common folder and delete the service folder.

cd C:\ADOWindowsServices
rd /s /q WindowsServiceTest

So, first, I have to add from the catalogue the Command Line task. So, click on the + in the Deployment group job tab and the list of tasks will appear. Search for this task and click Add.

Add Command Line task
Add Command Line task

Then, click on the task to see the settings. Change the Display name and the Script.

Command Line task settings
Command Line task settings

Although, it doesn’t seem allow to paste a YAML script, I can copy the commands from the script below and paste in the Script. This is the YAML of this step. Remember to rename WindowsServiceTest with the name of your service.

steps:
- script: |
   sc STOP WindowsServiceTest
   sc DELETE WindowsServiceTest

   C:
   IF NOT EXIST C:\ADOWindowsServices\WindowsServiceTest GOTO NODIR
   cd C:\ADOWindowsServices
   rd /s /q WindowsServiceTest
   
   :NODIR
    
  displayName: 'Uninstall Existing Service'

Extract files

Then, the next step is to extract the files from the zip file. Just as a reminder, in the previous post, I created a zip file from the build folder. Here, I’m going to unzip the file in the service folder.

So, click on the + in the Deployment group job tab and the list of tasks will appear. Search for Extract files and Add this task.

Add Extract files to the Release pipeline
Add Extract files to the Release pipeline

Now, in the setting of this task, the Destination folder is required.

Extract files destination folder
Extract files destination folder

In my case, the destination folder is C:\ADOWindowsServices\WindowsServiceTest. So, the setting result is

Example of the destination folder
Example of the destination folder

Install the service

Now, the last thing to do is to install and start the service. Again, I will use sc.exe to do that. Here is the script to copy and paste into the settings. Remember to change the name of the service to yours.

SC CREATE WindowsServiceTest start=auto binpath=C:\ADOWindowsServices\WindowsServiceTest\WindowsServiceTest.exe

SC description "WindowsServiceTest" "This is a Windows Service from Azure DevOps"
SC START WindowsServiceTest

I noticed that having a space in start= auto is working better than start=auto

Here is what it looks like.

Install new service and save pipeline
Install new service and save pipeline

Save

Last but not least, remember to Save the pipeline. Also, you can rename the New release pipeline with a more meaningful name.

Create a release

After all of that, finally, I can create my first release. After saving, the Create release is enabled.

Create a release enabled
Create a release enabled

When I click on this button, I see the configuration of the release.

Create a new release
Create a new release

After clicking Create, a message appears to communicate that the release has been created.

Release created
Release created

If I click on the release name (in the above screenshot Release-5), I can see the progress of the deployment. At the end of the process, I can see a screen like that.

Release has been deployed
Release has been deployed

In this screen, I can see all the details about the deployment, see the logs and redeploy this specific build.

Wrap up

I hope those 2 posts about Deploying and Releasing Windows Services using Azure DevOps are useful. Please let me know if you have any further questions or comments.

Related posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.