This post will demonstrate how to package a .Net Windows service application using Habitat. A Windows service application provides some interesting challenges to Habitat packaging because the application process is ultimately controlled by the Windows Service Control Manager (SCM). It runs outside of the Habitat Supervisor process tree. It also requires some initial setup when the Supervisor starts the application for the first time since the SCM service entry will not be present. These are similar to the challenges of packaging an IIS web application as described in this previous post.
Our Fancy Windows Service
To illustrate how DevSecOps can package and run Windows services with Habitat, we will use a VERY simple Windows service. You can find the full source code and an accompanying Habitat plan here.
I created the application in Visual Studio 2017 Community Edition starting with the code generated by File/New/Project
and selecting a “Windows Servce (.NET Framework)” application. I changed the *.cs
files with the service implementation described below and added a habitat
folder to include my plan.ps1
, hooks, and configuration. However, I made no changes to the .csproj
or other project files. This is very much a “vanilla” C# Visual Studio Windows service.
This service simply logs the state of the service to a file. It logs startup and shutdown messages as well as a message once a minute stating that the application is up. Not a very useful service but fine for illustrating how a Windows service looks inside of Habitat.
The Plan
windows-service-sample/habitat/plan.ps1 link
$pkg_name="windows-service-sample" $pkg_origin="mwrock" $pkg_version="0.1.0" $pkg_maintainer="Matt Wrock" $pkg_license=@('MIT') $pkg_description="A sample .NET Windows Service" $pkg_bin_dirs=@("bin") function Invoke-Build { Copy-Item $PLAN_CONTEXT/../* $HAB_CACHE_SRC_PATH/$pkg_dirname -recurse -force -Exclude ".vs" ."$env:SystemRoot\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe" $HAB_CACHE_SRC_PATH/$pkg_dirname/${pkg_name}.csproj /t:Build /p:Configuration=Release if($LASTEXITCODE -ne 0) { Write-Error "dotnet build failed!" } } function Invoke-Install { Copy-Item $HAB_CACHE_SRC_PATH/$pkg_dirname/bin/release/* $pkg_prefix/bin }
Because our service is so simple and has no dependencies, there is not much going on here. Of course our application depends on the standard .Net framework but because that is a core operating system framework, we do not bother isolating it with Habitat. Thats fine because any modern Windows OS from 2008 R2 and up should have the .Net 4 CLR installed.
In short, this plan copies the source code to the Habitat cache, performs an MSBUILD
release build and then copies the binaries to our Habitat package staging folder.
Creating the Windows Service from the Supervisor
When a Habitat Supervisor first installs our application, it likely will not have an actual service installed in the machine’s SCM. Our init
hook will need to check if one is installed and install it if one is not present:
windows-service-sample/habitat/hooks/init link
Set-Location {{pkg.svc_path}} if(Test-Path bin) { Remove-Item bin -Recurse -Force } New-Item -Name bin -ItemType Junction -target "{{pkg.path}}/bin" | Out-Null # Add the Windows Service if((Get-Service HabSampleService -ErrorAction SilentlyContinue) -eq $null) { $binPath = (Resolve-Path "{{pkg.svc_path}}/bin/{{pkg.name}}.exe").Path &$env:systemroot\system32\sc.exe create HabSampleService binpath= $binPath }
This simply uses the sc.exe
utility to install the service. Note that we want to keep the default manual
startup setting and do not want the service to start automatically
when Windows boots. This is because we want the Habitat Supervisor to control when the service starts and stops.
Starting the service in a Run Hook
Our run
hook will start the service and loop continuously until the service is stopped.
windows-service-sample/habitat/hooks/run link
Copy-Item "{{pkg.svc_config_path}}\windows-service-sample.exe.config" "{{pkg.svc_path}}\bin" -Force Start-Service HabSampleService Write-Host "{{pkg.name}} is running" while($(Get-Service HabSampleService).Status -eq "Running") { Start-Sleep -Seconds 1 }
First we copy our templatized configuration file that ensures that status messages will be logged to the pkg.svc_data_path
. Next we simply call Start-Service
to start our service. We will keep the process running as long as the service is in the running
state. So if one were to manually stop the service from the SCM, the Habitat service would also terminate and attempt to restart it.
Cleaning Up the Service
Now we will take advantage of a new Habitat feature: the post-stop
hook. This hook runs after a Habitat service is stopped. Here we make sure that the actual Windows service stops when the Habitat service stops:
windows-service-sample/habitat/hooks/post-stop link
if($(Get-Service HabSampleService).Status -ne "Stopped") { Write-Host "{{pkg.name}} stopping..." Stop-Service HabSampleService Write-Host "{{pkg.name}} has stopped" }
We especially need this for upgrade scenarios when a Supervisor updates a running service with an updated version of our service application. An update would fail if the service was running.
Building and Testing our .Net Service Application
Now lets see if this all works. Lets clone the repository and run hab studio enter
from its root to start a Windows Habitat Studio. I’m on Windows 10 running Docker Community Edition for Windows so this command will spin up a Windows Server 2016 Core container:
C:\windows-service-sample
C:\windows-service-sample> hab studio enter hab-studio: Creating Studio at c:/ hab-studio: Entering Studio at c:/ ** The Habitat Supervisor has been started in the background. ** Use 'hab svc start' and 'hab svc stop' to start and stop services. ** Use the 'Get-SupervisorLog' command to stream the Supervisor log. ** Use the 'Stop-Supervisor' to terminate the Supervisor. [HAB-STUDIO] Habitat:\src>
This is especially nice for testing Windows services because I don’t have to worry about random services being installed into my personal SCM. You could alternatively run hab studio enter -w
to start a local Windows Studio (not containerized).
Now we will run build .
to build our Windows service package. We can see MSBUILD
starting:
Building the package
[HAB-STUDIO] Habitat:\src> build . : Loading C:\src\habitat\plan.ps1 windows-service-sample: Plan loaded windows-service-sample: Validating plan metadata windows-service-sample: hab-plan-build.ps1 setup windows-service-sample: Using HAB_BIN=C:\hab\pkgs\core\hab-studio\0.51.0\20171218132802\bin\hab\hab.exe for installs, signing, and hashing windows-service-sample: Resolving scaffolding dependencies windows-service-sample: Setting PATH=C:\hab\pkgs\mwrock\windows-service-sample\0.1.0\20171220175451/bin;;C:\hab\pkgs\core\hab-studio\0.51.0\20171218132802\bin\hab;C:\hab\pkgs\core\hab-studio\0.51.0\20171218132802\bin\7zip;C:\hab\pkgs\core\hab-studio\0.51.0\20171218132802\bin;C:\Windows\system32;C:\Windows windows-service-sample: Setting LIB= windows-service-sample: Setting INCLUDE= windows-service-sample: Clean the cache windows-service-sample: Preparing to build windows-service-sample: Building Microsoft (R) Build Engine version 4.6.1586.0 [Microsoft .NET Framework, version 4.0.30319.42000] Copyright (C) Microsoft Corporation. All rights reserved. Build started 12/20/2017 5:54:53 PM. Project "C:\hab\cache\src\windows-service-sample-0.1.0\windows-service-sample.csproj" on node 1 (Build target(s)). ...
Ok, lets start the service with the Studio’s Supervisor:
Starting the studio’s supervisor
[HAB-STUDIO] Habitat:\src> hab svc load mwrock/windows-service-sample hab-sup(MN): Supervisor starting mwrock/windows-service-sample. See the Supervisor output for more details.
We should see an actual Windows service running in the scm:
Check for the service
[HAB-STUDIO] Habitat:\src> Get-Service HabSampleService Status Name DisplayName ------ ---- ----------- Running HabSampleService HabSampleService
We should also see the fabulous status messages in the service’s data file:
View the status message
[HAB-STUDIO] Habitat:\src> cat C:/hab/svc/windows-service-sample/data/status.txt HabSampleService is starting HabSampleService is started HabSampleService is up HabSampleService is up
Now we will stop the service:
Stopping the service
[HAB-STUDIO] Habitat:\src> hab svc stop mwrock/windows-service-sample [HAB-STUDIO] Habitat:\src> Get-Service HabSampleService Status Name DisplayName ------ ---- ----------- Stopped HabSampleService HabSampleService [HAB-STUDIO] Habitat:\src> cat C:/hab/svc/windows-service-sample/data/status.txt HabSampleService is starting HabSampleService is started HabSampleService is up HabSampleService is up HabSampleService is stopped
As you can see, the service is stopped and it logs that status to its data file.