Services Deep Dive
Windows services run in the background without user interaction — web servers, monitoring agents, database engines. This tutorial covers everything you need to install, configure, and control services with FalkForge.
Basic Service Installation
In MSI Basics you saw a service defined inside demo 03. Here is the essential pattern:
package.Service("AcmeServer", svc =>
{
svc.DisplayName = "Acme Server";
svc.Description = "Acme Client-Server Suite background service";
svc.Executable = "server.exe";
svc.StartMode = ServiceStartMode.Automatic;
svc.Account = ServiceAccount.LocalSystem;
svc.Arguments = "--port=8080 --config=appsettings.json";
});
Four things happen here:
- Service name — the internal name Windows uses to identify the service (
AcmeServer). - Display name — the human-readable label shown in
services.msc. - Executable — the binary that Windows launches.
- Start mode and account — when the service starts and which identity it runs under.
This is enough for many installers. Demo 17 goes further.
Service Configuration
Demo 17 installs two services with different configurations. Let's walk through the first one.
Name, display name, and description
package.Service("DemoService", svc =>
{
svc.DisplayName = "Demo Background Service";
svc.Description = "Demonstrates FalkForge service installation";
svc.Executable = "[ProgramFilesFolder]Demo\\ServiceDemo\\myservice.exe";
The service name (DemoService) is the key you use with sc query, net start, and dependency declarations. Keep it short, no spaces. The display name and description appear in the Services management console.
Start type
svc.StartMode = ServiceStartMode.Automatic;
FalkForge supports four start types:
Automatic— starts at boot. Use for services that must always run.AutomaticDelayed— starts shortly after boot. Reduces boot time contention.Manual— starts only when another service or application requests it.Disabled— cannot start until an admin changes the setting.
Most background services should use AutomaticDelayed. Use Automatic only when other services depend on yours at boot.
Service account
svc.Account = ServiceAccount.LocalService;
The account determines the security context your service runs in.
LocalSystem— full access to the machine. Dangerous. Use only when you need kernel-level operations.LocalService— limited local privileges, anonymous network identity. Good default for services that don't need network access.NetworkService— limited local privileges, machine identity on the network. Use when the service must authenticate to other machines.
Deep dive: Service account security
Choosing the right account is the most important security decision for a service.
LocalSystem has unrestricted access. A compromised LocalSystem service gives an attacker full control of the machine. Avoid it unless you are writing a driver-level service or need to impersonate any user.
LocalService runs with the same privileges as a standard user. It can read most file system locations but cannot write outside its own profile. On the network, it appears as an anonymous user. This is the safest built-in choice.
NetworkService has the same local privileges as LocalService. The difference is network identity: it authenticates as the computer account in Active Directory. Use it for services that call other servers, access network shares, or connect to SQL Server with Windows authentication.
Custom domain accounts are best for enterprise services. Create a dedicated Active Directory service account with only the permissions the service needs. FalkForge supports this via AccountProperty:
svc.AccountProperty("[SERVICEACCOUNT]");
svc.Password = "[DEMO_PASSWORD]";
The installer reads the account name from an MSI property at install time. The password property is passed securely and never written to the MSI database in clear text.
Conditional installation
svc.Condition("INSTALLSERVICE ~= \"true\"");
The service is only installed when the INSTALLSERVICE property is set. This lets you gate service installation behind a checkbox in the UI or a command-line parameter.
Service permissions
svc.Permission(perm =>
{
perm.Domain = "BUILTIN";
perm.User = "Administrators";
perm.Permission = 0xF01FF; // SERVICE_ALL_ACCESS
});
This grants the Administrators group full control over the service. The permission value is a standard Windows access mask. Common values: 0xF01FF (full control), 0x2009D (start, stop, query status).
Custom domain account
The second service in demo 17 runs under a domain account:
package.Service("DemoWorker", svc =>
{
svc.DisplayName = "Demo Worker Service";
svc.Executable = "[ProgramFilesFolder]Demo\\ServiceDemo\\myservice.exe";
svc.StartMode = ServiceStartMode.Manual;
svc.AccountProperty("[SERVICEACCOUNT]");
svc.Password = "[DEMO_PASSWORD]";
});
AccountProperty reads the account name from an MSI property. The installer UI or command line sets SERVICEACCOUNT and DEMO_PASSWORD at install time. This avoids hardcoding credentials in the installer.
Service Control
Installing a service registers it with Windows. But you also need to tell Windows when to start and stop it during install and uninstall.
package.ServiceControl(sc =>
{
sc.ServiceName("DemoService");
sc.StopOnUninstall();
sc.StartOnInstall();
sc.DeleteOnUninstall();
sc.Wait(true);
});
This configuration does three things:
- Start after install — the service starts automatically once files are copied.
- Stop before uninstall — the service is stopped before its files are removed.
- Delete on uninstall — the service registration is removed from Windows.
Wait(true) tells Windows Installer to wait for the service to reach the desired state before continuing. Without it, the installer moves on immediately and later steps may fail if they depend on the service being stopped.
Passing arguments on start
package.ServiceControl(sc =>
{
sc.ServiceName("DemoWorker");
sc.StopOnInstall();
sc.StartOnInstall();
sc.Arguments("--config=[INSTALLDIR]config.json");
sc.DeleteOnUninstall();
});
Notice StopOnInstall() here. During an upgrade, the worker is stopped first, files are updated, then the service is restarted. The Arguments method passes command-line arguments when the service starts. MSI properties like [INSTALLDIR] are resolved at install time.
Deep dive: MSI service control sequence
Windows Installer runs service operations in a specific order during the execute sequence:
- StopServices — stops services marked for stopping.
- DeleteServices — removes service registrations marked for deletion.
- (file operations happen here — copy, remove, overwrite)
- InstallServices — registers new services with the Service Control Manager.
- StartServices — starts services marked for starting.
This ordering ensures services are stopped before their files are touched and started only after new files are in place. FalkForge maps your StopOnInstall, StartOnInstall, StopOnUninstall, and DeleteOnUninstall calls to the correct MSI table entries so Windows Installer handles the sequencing automatically.
If your service is being upgraded, the sequence is: stop old service, delete old registration, copy new files, install new service registration, start new service.
Failure Recovery
Services crash. The Windows Service Control Manager can automatically recover from failures. FalkForge exposes this through FailureActions.
Restart on failure
svc.FailureActions(fa =>
{
fa.OnFirstFailure = FailureAction.Restart;
fa.OnSecondFailure = FailureAction.Restart;
fa.OnSubsequentFailures = FailureAction.None;
fa.ResetPeriod = TimeSpan.FromDays(1);
fa.RestartDelay = TimeSpan.FromSeconds(30);
});
This tells Windows: restart the service on the first two failures, then give up. Wait 30 seconds before each restart attempt. After 24 hours without a failure, reset the failure counter.
Run a command on failure
svc.FailureActions(fa =>
{
fa.OnFirstFailure = FailureAction.RunCommand;
fa.Command = "[ProgramFilesFolder]Demo\\ServiceDemo\\myservice.exe --diagnose";
fa.OnSecondFailure = FailureAction.Restart;
fa.OnSubsequentFailures = FailureAction.Reboot;
fa.RebootMessage = "Demo Worker service has failed repeatedly. Rebooting.";
});
This escalates through three stages:
- First failure — run a diagnostic command to capture state.
- Second failure — restart the service.
- Subsequent failures — reboot the machine (with a message broadcast to logged-in users).
Four failure actions are available:
None— do nothing.Restart— restart the service afterRestartDelay.RunCommand— execute the program specified byCommand.Reboot— reboot the machine. Use with extreme caution.
Deep dive: Service Control Manager failure recovery model
The Service Control Manager (SCM) tracks a failure count for each service. Every time the service process exits unexpectedly, the count increments and the corresponding action fires.
The reset period controls how long the SCM waits before resetting the failure count to zero. If the service runs successfully for the entire reset period, the next failure is treated as a first failure again. Set this to at least one day. Shorter periods may mask recurring problems.
The restart delay applies only to the Restart action. It prevents a crash loop from consuming all system resources. A 30-second delay is a reasonable starting point. For services that initialize slowly, use a longer delay.
RunCommand runs in the context of the LocalSystem account, regardless of which account the service runs under. The command has access to the %SystemRoot% environment variable but not the service's environment. Use it for diagnostics, alerting, or cleanup scripts.
Reboot should be reserved for critical infrastructure services. The reboot message is broadcast to all interactive sessions before the machine restarts. Users get a brief window to save work.
Service Dependencies
Services often depend on other services. A web server needs TCP/IP. A database client needs the network stack. FalkForge lets you declare these dependencies so Windows starts services in the right order.
Depend on a specific service
svc.DependsOn("Tcpip");
The service will not start until the Tcpip service is running. If Tcpip fails to start, your service won't start either.
Depend on a service group
svc.DependsOnGroup("NetworkProvider");
Service groups are collections of related services. Depending on a group means your service waits until at least one member of the group has started. Common groups include NetworkProvider, TDI, and NDIS.
You can combine both. Demo 17's first service depends on the Tcpip service and the NetworkProvider group:
svc.DependsOn("Tcpip");
svc.DependsOnGroup("NetworkProvider");
Deep dive: Service startup order and dependency chains
When Windows boots, the Service Control Manager builds a dependency graph of all automatic services. It starts services in topological order — dependencies first, then dependents.
Dependencies are transitive. If service A depends on B, and B depends on C, then A won't start until both B and C are running.
Circular dependencies prevent all involved services from starting. Windows logs an error and skips them. Always verify your dependency chain doesn't create a cycle.
Timeout: Windows gives each service 30 seconds to report that it has started. If a dependency doesn't start within this window, dependent services may fail to start. Services that need longer initialization should use SERVICE_START_PENDING with periodic status updates.
Group dependencies are satisfied when any one member of the group starts. This is useful when you need a capability (like network connectivity) but don't care which specific driver or service provides it.
Secure Service Credentials
When a service runs under a domain or custom account, you need to collect and transport the password securely. FalkForge provides a complete secure pipeline: PasswordBridge collects from a PasswordBox, DPAPI protects in memory, SetSecureProperty transports via named pipe, and IMsiApi delivers to Windows Installer — the password never appears on a command line or in a log file.
Step 1: XAML — Collect with PasswordBridge
<PasswordBox ui:PasswordBridge.Key="ServicePassword" />
PasswordBridge is an attached property that bridges WPF's PasswordBox (which deliberately doesn't support data binding for security) to FalkForge's secure storage. The password stays in PasswordBox's internal SecureString — it's never a C# string.
Deep dive: Why PasswordBox doesn't support binding
WPF's PasswordBox intentionally omits a Password dependency property. This is security by design: .NET strings are immutable and interned, so a password stored as a string persists in memory long after you're done with it. You can't zero it, and the garbage collector may copy it during compaction.
PasswordBridge works around this by reading the password only when explicitly requested (via GetPassword), wrapping it in SensitiveBytes that zeros memory on disposal. The password exists in managed memory for the shortest possible window.
Step 2: Page code — Store with DPAPI
using var pw = GetPassword("ServicePassword");
if (!pw.IsEmpty)
SharedState.SetSensitive("ServicePassword", pw.Span);
GetPassword returns SensitiveBytes — a disposable wrapper that zeros memory when done. SetSensitive encrypts with Windows DPAPI before storing in InstallerState.
Deep dive: How DPAPI CurrentUser scope works
The Data Protection API (DPAPI) is a Windows built-in that encrypts data using a key derived from the current user's logon credentials. SetSensitive calls ProtectedData.Protect with DataProtectionScope.CurrentUser, meaning only the same Windows user account on the same machine can decrypt it.
This protects the password while it sits in InstallerState between pages. Even if another process reads the installer's memory, it gets ciphertext. The original bytes are zeroed immediately after encryption.
Step 3: Progress page — Transport via named pipe
using var pw = SharedState.GetSensitive("ServicePassword");
if (!pw.IsEmpty)
Engine.SetSecureProperty("SERVICEPASSWORD", pw);
SetSecureProperty sends the password over the HMAC-authenticated named pipe as a binary message. It's stored in the engine's VariableStore as a SecureVariable — pinned memory that's zeroed on dispose. It never appears in logs.
Deep dive: SetSecureProperty vs SetProperty
SetProperty stores the value as a plain string. It's visible in process listings, may appear in MSI log files, and is passed to msiexec.exe command-line arguments where any process on the machine can read it via the Win32 QueryFullProcessImageName / command-line APIs.
SetSecureProperty stores the value as a SecureVariable — a disposable wrapper over pinned memory. It's delivered to Windows Installer via the IMsiApi P/Invoke interface (MsiSetProperty), never via command-line arguments. The engine's logger explicitly skips secure properties. When the engine shuts down, SecureVariable.Dispose zeros the memory via CryptographicOperations.ZeroMemory.
For any value that is a secret — passwords, API keys, connection strings with credentials — always use SetSecureProperty.
Step 4: Service definition — Reference the property
package.Service("MyService", svc =>
{
svc.AccountProperty(".\\" + "[SERVICEACCOUNT]");
svc.Password = "[SERVICEPASSWORD]";
});
The service definition references MSI properties with bracket syntax. At install time, the engine resolves [SERVICEPASSWORD] from the secure variable store and passes it to Windows Installer via the IMsiApi P/Invoke — never via msiexec.exe command-line arguments.
Security warning: Never hardcode passwords in service definitions or MSI property defaults. Never use SetProperty() for passwords — it stores the value as a plain string visible in process listings. Always use the PasswordBridge → SetSensitive → SetSecureProperty pipeline shown above.
See demo/14-lifecycle-hooks for the complete working example of this pattern.
Recap
Here are the common service installation patterns:
- Web server —
AutomaticDelayedstart,NetworkServiceaccount, depends onTcpip, restart on first two failures. - Background worker —
Manualstart,LocalServiceaccount, no dependencies, run diagnostic command on failure. - System agent —
Automaticstart,LocalSystemaccount (only if required), depends on service group, escalating failure actions.
The full source is in demo/17-services/Program.cs. Run it with:
dotnet run --project demo/17-services