Repository navigation
Embedding in Custom Installers
- Overview
- Why Servy Requires Automated Initialization
- WiX Toolset (MSI) Integration
- Inno Setup Integration
- Advanced Installer & InstallShield
- Exit Codes, Re-Runs & Cleanup
- Verification & Troubleshooting
When packaging software into enterprise installers (such as WiX, Inno Setup, Advanced Installer, or InstallShield), background services are typically installed by writing registry keys or using built-in service tables.
Legacy wrappers like NSSM are not secure for production environments because NSSM stores the service configuration, including application parameters and environment variables, in plaintext in the Windows Registry, where anyone with read access to the registry can inspect it. In contrast, Servy encrypts sensitive fields (the account password, the process and hook parameters, and the environment variables) with AES-256 and HMAC-SHA256 authenticated encryption under a DPAPI-protected, HKDF-derived key, and each service account can reach only its own configuration and logs (see Security).
Servy functions as an enterprise service host rather than a standalone runner script. To maintain production stability, security, and real-time monitoring, services must be installed via the command-line tool (servy-cli.exe) during the installation phase and removed during the uninstallation phase.
The servy-cli.exe utility exposes the full service configuration, including process execution settings, environment variables, logging and log rotation, health monitoring and automated recovery, service dependencies, the service account, and pre/post launch and stop hooks. See Servy CLI for every install option.
To include servy-cli.exe in your installer package:
- Extract
servy-cli.exedirectly from the modern (.NET 10.0+) build portable package (servy-x.x-x64-portable.7zorservy-x.x-arm64-portable.7z), choosing the one that matches the architecture of the target machines. Servy is built for x64 and ARM64 only; there is no 32-bit build. -
servy-cli.exeis completely self-contained and does not require any external runtime dependencies or pre-installed frameworks (.NET runtime is embedded), making it ideal for clean, standalone installer distributions. It is a single file:appsettings.cli.jsonis optional and nothing else needs to be shipped next to it. -
servy-cli.execarries the rest of Servy inside it. The firstinstall,start,restartorimportcommand extracts the service wrapper (Servy.Service.CLI.exe),Servy.Restarter.exe, the host service binary (Servy.Host.exe) and Sysinternalshandle64.exe(handle64a.exeon ARM64) into%ProgramData%\Servy, then installs and starts theServyhost service. Do not ship or copy these files yourself, and do not install them to your application folder. -
.NET Framework 4.8 build: if you embed the legacy build (
servy-x.x-net48-x64-portable.7z) instead, itsservy-cli.exeis not a single file: ship it together with the*.dllfiles from the same package, and make sure .NET Framework 4.8 is installed on the target machine. The binaries it extracts are namedServy.Service.CLI.Net48.exe,Servy.Restarter.Net48.exeandServy.Host.Net48.exe. -
Elevation is required.
servy-cli installrefuses to run without administrator rights, and creating, starting, stopping or deleting a Windows service needs them anyway. In an MSI, this means a deferred custom action withImpersonate="no", which runs asLocalSystem. -
The service name
Servyis reserved for the Servy host service and is rejected byinstall. Pick any other name for your service.
Standard legacy wrappers (like srvany or NSSM) rely solely on static registry keys. Servy requires explicit initialization through servy-cli install, immediate startup via servy-cli start, clean shutdown via servy-cli stop, and proper cleanup via servy-cli uninstall because it automatically configures:
-
Central Host Windows Service (
Servy): Installs theServyhost service (Servy.Host.exe) when it is missing, starts it, and makes every Servy service depend on it. Your service gets its configuration from this host over a local named pipe. -
Encrypted Vault & SQLite Database: Creates
%ProgramData%\Servy\db\Servy.db, the SQLite database that holds the configuration of each service (sensitive fields encrypted) and its runtime state. -
DPAPI & AES-256 Key Material: Creates the machine-wide master key
%ProgramData%\Servy\security\aes_key.dat, protected with DPAPI in machine scope and bound to the machine. The AES-256 encryption and HMAC-SHA256 authentication sub-keys are derived from it with HKDF. The key cannot be copied to another machine. -
Directory & Pipe ACL Hardening: Applies strict Windows Access Control Lists (ACLs) to
%ProgramData%\Servy, the extracted binaries, the log folder of each service (logs\services\<ServiceName>\) and the host's named pipe, so a service account gets only the access its own service needs. See Executable Permission Hardening.
Attempting to register or remove the Servy wrapper (Servy.Service.CLI.exe) directly via raw registry writes or the MSI ServiceInstall table bypasses these steps, causing service launch or uninstallation failures: a service registered that way has no record in Servy.db to load its configuration from, and a Servy service removed that way leaves its record and access grants behind.
To bundle Servy into a WiX installer without opening command prompt windows for the user, use deferred, elevated Quiet Execution Custom Actions (CAQuietExec or WixQuietExec).
Place servy-cli.exe alongside your application binaries, then configure your .wxs source file as follows. The sample uses WiX v3 syntax; CAQuietExec and the WixCA binary come from WixUtilExtension, so pass -ext WixUtilExtension to both candle and light.
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Fragment>
<!-- 1. Define command lines for silent installation, starting, stopping, and uninstallation.
A deferred custom action reads its command line from the property that has the same Id,
so each SetProperty is scheduled in the execute sequence just before its custom action. -->
<!-- Command line to install the service.
[INSTALLFOLDER] ends with a backslash, and a backslash right before a closing quote escapes the quote,
so the startup directory is written as "[INSTALLFOLDER]." rather than "[INSTALLFOLDER]". -->
<SetProperty Id="InstallServyService" Before="InstallServyService" Sequence="execute" Value=""[INSTALLFOLDER]servy-cli.exe" install --name="MyService" --path="[INSTALLFOLDER]myapp.exe" --startupDir="[INSTALLFOLDER]." --startupType="Automatic" --params="--port 8080 --env production""/>
<!-- Command line to start the service immediately after registration -->
<SetProperty Id="StartServyService" Before="StartServyService" Sequence="execute" Value=""[INSTALLFOLDER]servy-cli.exe" start --name="MyService""/>
<!-- Command line to stop the service prior to unregistration to release file locks -->
<SetProperty Id="StopServyService" Before="StopServyService" Sequence="execute" Value=""[INSTALLFOLDER]servy-cli.exe" stop --name="MyService""/>
<!-- Command line to silently unregister and clean up the service on uninstall -->
<SetProperty Id="UninstallServyService" Before="UninstallServyService" Sequence="execute" Value=""[INSTALLFOLDER]servy-cli.exe" uninstall --name="MyService""/>
<!-- 2. Declare elevated Quiet Execution Custom Actions -->
<!-- Executed during installation to register the service -->
<CustomAction BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Id="InstallServyService" Impersonate="no" Return="check"/>
<!-- Executed during installation to start the service immediately after registration -->
<CustomAction BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Id="StartServyService" Impersonate="no" Return="ignore"/>
<!-- Executed during uninstallation to stop the service before unregistration -->
<CustomAction BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Id="StopServyService" Impersonate="no" Return="ignore"/>
<!-- Executed during uninstallation to cleanly unregister the service -->
<CustomAction BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Id="UninstallServyService" Impersonate="no" Return="ignore"/>
<!-- 3. Sequence execution during install and uninstall -->
<InstallExecuteSequence>
<!-- Execute installation and start passes after files are copied to disk -->
<Custom Action="InstallServyService" After="InstallFiles">NOT Installed AND NOT PATCH</Custom>
<Custom Action="StartServyService" After="InstallServyService">NOT Installed AND NOT PATCH</Custom>
<!-- Execute stop and uninstall passes during uninstallation.
The stop pass also runs when a major upgrade removes the previous version, so the service releases
its files before they are replaced; the uninstall pass is skipped then, and the new version's
install pass updates the existing service in place. -->
<Custom Action="StopServyService" Before="UninstallServyService">REMOVE="ALL"</Custom>
<Custom Action="UninstallServyService" Before="RemoveFiles">REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE</Custom>
</InstallExecuteSequence>
</Fragment>
</Wix>Note
Setting Execute="deferred" with Impersonate="no" ensures the installer executes servy-cli as LocalSystem, with the administrative privileges needed to create the %ProgramData%\Servy vault and to install, start, stop, or unregister Windows services. An immediate or impersonated custom action runs as the installing user, who on a UAC-enabled machine typically has no elevated token, so servy-cli install fails its elevation check.
Exit codes. servy-cli exits with 0 on success and a non-zero code on failure (see Exit Codes, Re-Runs & Cleanup). Return="check" on InstallServyService makes the MSI fail and roll back when the service cannot be installed. The stop and uninstall passes use Return="ignore" because they exit with 1 when the service does not exist, which must not block an uninstall. StartServyService also uses Return="ignore", so a service that does not reach the Running state within its start timeout leaves the installation in place; change it to Return="check" if a failed start should fail the setup. The sample defines no rollback action: if a later action fails after InstallServyService succeeded, the service stays registered unless you add a matching Execute="rollback" custom action that runs servy-cli uninstall.
Major upgrades. The upgrade handling above relies on the default <MajorUpgrade /> scheduling (afterInstallValidate), which removes the previous version before the new files are installed. The NOT UPGRADINGPRODUCTCODE condition must already be present in the version being upgraded from, because the removal runs that version's custom actions. Running servy-cli install for a service that already exists updates its configuration instead of failing, and the configuration takes effect the next time the service starts.
Warning
Keep secrets out of the MSI log. Windows Installer writes property values and CustomActionData to verbose logs (/l*v). If a command line carries a secret (--password, or --params / --envVars holding credentials), hide it: declare the property hidden (<Property Id="InstallServyService" Hidden="yes" />) and add HideTarget="yes" to the custom action. The command line is still visible in the process list while servy-cli.exe runs. servy-cli can read these values from environment variables instead (SERVY_PASSWORD, SERVY_PROCESS_PARAMETERS, SERVY_ENVIRONMENT_VARIABLES, ...), but a quiet-execution custom action cannot set them; an alternative is to install from a configuration file with servy-cli import --config=xml --path="..." --install and delete the file afterwards (see Export/Import Services and Security).
Inno Setup supports executing administrative commands directly using the [Run] and [UninstallRun] sections. The setup must run elevated, so keep PrivilegesRequired=admin (the Inno Setup default) in the [Setup] section.
Add the following sections to your .iss script:
[Files]
; Include servy-cli.exe in the installation package (a single file for the .NET 10 build)
Source: "servy\servy-cli.exe"; DestDir: "{app}"; Flags: ignoreversion
; Your application's own files
Source: "bin\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
[Run]
; Silently register and initialize the service during installation
Filename: "{app}\servy-cli.exe"; \
Parameters: "install --name=""MyService"" --path=""{app}\myapp.exe"" --startupDir=""{app}"" --startupType=""Automatic"" --params=""--port 8080"""; \
Flags: runhidden waituntilterminated; \
StatusMsg: "Registering background service..."
; Silently start the service immediately after registration
Filename: "{app}\servy-cli.exe"; \
Parameters: "start --name=""MyService"""; \
Flags: runhidden waituntilterminated; \
StatusMsg: "Starting background service..."
[UninstallRun]
; Stop the active service process first to release file locks
Filename: "{app}\servy-cli.exe"; \
Parameters: "stop --name=""MyService"""; \
Flags: runhidden waituntilterminated; \
RunOnceId: "StopMyService"
; Silently unregister and clean up the service during uninstallation
Filename: "{app}\servy-cli.exe"; \
Parameters: "uninstall --name=""MyService"""; \
Flags: runhidden waituntilterminated; \
RunOnceId: "UninstallMyService"
[Code]
// When the setup runs over an existing installation, stop the service before its files are replaced.
// On a first install servy-cli.exe is not there yet, so nothing runs.
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
if FileExists(ExpandConstant('{app}\servy-cli.exe')) then
Exec(ExpandConstant('{app}\servy-cli.exe'), 'stop --name="MyService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Result := '';
end;Using Flags: runhidden waituntilterminated ensures that installation and uninstallation wait for complete vault generation and service management tasks without showing console windows to the user.
Keep the following in mind:
-
Exit codes are not checked. Inno Setup logs the exit code of a
[Run]or[UninstallRun]entry but carries on whatever it is. If a failedservy-cli installmust fail the setup, run it from[Code]withExecand checkResultCode(0is success). -
Upgrades. Running the setup again over an existing installation runs the
[Run]entries again.servy-cli installupdates the existing service in place instead of failing, andstartthen starts it with the new configuration. ThePrepareToInstallfunction above stops the service first, so its files are not locked when they are replaced. -
{app}has no trailing backslash, so--startupDir=""{app}""is safe. Never pass a quoted path that ends with a backslash: the backslash escapes the closing quote.
- Go to the Custom Actions page.
- Add a new Launch File action under
InstallExecuteSequence->Add Resources. - Set File Path to
[#servy-cli.exe]. - Set Command Line (Installation).
[APPDIR]ends with a backslash, so the startup directory is written as"[APPDIR].": a backslash right before a closing quote escapes the quote.install --name="MyService" --path="[APPDIR]myapp.exe" --startupDir="[APPDIR]." --startupType="Automatic" --params="--port 8080" - Set Execution Options to Deferred with no impersonation (to ensure administrative elevation) and check Hide console window.
- Add a second Launch File action right after installation to start the service, with this command line:
start --name="MyService" - Under the
Uninstallsequence, add two more Launch File actions with the same execution options, the first to stop the service and the second to unregister it:stop --name="MyService"uninstall --name="MyService" - Condition the install and start actions with
NOT Installed AND NOT PATCH, the stop action withREMOVE="ALL", and the uninstall action withREMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE, so an upgrade does not unregister the service (see WiX Toolset (MSI) Integration).
- Navigate to Behavior and Logic -> Custom Actions and Sequences.
- Right-click Custom Actions and select New Executable -> Installed with Product.
servy-cli.exemust be installed with your product, because the uninstall actions run it from the installation folder. - Set Target to
[INSTALLDIR]servy-cli.exe. - Set Command Line Arguments (Installation).
[INSTALLDIR]ends with a backslash, so the startup directory is written as"[INSTALLDIR].": a backslash right before a closing quote escapes the quote.install --name="MyService" --path="[INSTALLDIR]myapp.exe" --startupDir="[INSTALLDIR]." --startupType="Automatic" --params="--port 8080" - Add a second custom action to start the service immediately after install, with these arguments:
start --name="MyService" - Add two custom actions to the uninstallation sequence, the first to stop the service and the second to unregister it, with these arguments:
stop --name="MyService"uninstall --name="MyService" - Set In-Script Execution to Deferred Execution in System Context on all four actions.
- Condition the install and start actions with
NOT Installed AND NOT PATCH, the stop action withREMOVE="ALL", and the uninstall action withREMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE, so an upgrade does not unregister the service (see WiX Toolset (MSI) Integration).
servy-cli is designed to be driven by scripts and installers. Its behavior in the cases an installer author runs into:
| Exit code | Meaning |
|---|---|
0 |
The command succeeded. |
1 |
The command failed (invalid option, missing elevation, service not found, timeout, cancellation, unexpected error). The reason is printed to the console, which a quiet-execution custom action copies into the MSI log. |
2 |
Incompatible environment: the SQLite library is older than the minimum version Servy requires. |
-
installon an existing service updates its configuration in place instead of failing, so it is safe to run again on a repair or an upgrade. A running service picks up the new configuration the next time it starts. -
startsucceeds at once when the service is already running, and fails when the service does not reach theRunningstate within its start timeout. -
stopsucceeds at once when the service is already stopped. It fails with exit code1when Servy does not know the service. -
uninstallstops the service itself and waits for it to stop before deleting it; if the service does not stop within its stop timeout, the uninstall is aborted with exit code1rather than leaving the service marked for deletion. It fails with exit code1when the service does not exist. Runningstopfirst, as in the samples above, keeps the stop explicit in the setup log. -
No console interaction is needed. The spinner is turned off automatically when there is no interactive console, as in a hidden custom action;
--quiet(-q) turns it off explicitly. -
What uninstalling your service leaves behind.
servy-cli uninstallremoves your service from the Service Control Manager and fromServy.db, and takes back the access its service account was granted (see Executable Permission Hardening). It does not remove theServyhost service, the binaries extracted into%ProgramData%\Servy, the database, the encryption key or the logs (the log folder of your service,logs\services\<ServiceName>\, is kept for the administrators), because other Servy services on the machine may still use them.servy-clihas no command to remove the host service. Servy's own uninstaller removes theServyhost service and the extracted*.exeand*.dllfiles only when no Servy-managed service remains, and always keepsdb\,security\andlogs\; if you remove them from your own uninstaller, apply the same check. Never deletesecurity\aes_key.datwhileServy.dbis kept: the configuration in the database cannot be decrypted without it.
After running your custom installer, verify that setup, startup, and removal succeed cleanly:
-
Verify SCM Registration & Running State: Run
sc query MyServiceor checkservices.mscto confirm the service is registered and currently in theRUNNINGstate after installation.servy-cli status --name="MyService"reports the same from the command line. -
Verify Uninstallation: Uninstall the application via Add/Remove Programs and run
sc query MyServiceto ensure the service is completely removed without leaving orphaned entries. -
Verify Vault Initialization: Confirm that the
ServyWindows service is running (sc query Servy) and that the vault files are created in%ProgramData%\Servy(db\Servy.db,security\aes_key.dat) with restricted ACLs (see Security). -
Check for Locked Files (
DELETE_PENDING): Ensure the service is stopped before your application files are removed or replaced (byservy-cli stop, or byservy-cli uninstall, which stops it first) so the binaries in the application folder can be removed without requiring a system reboot. -
Review Installer Logs: If the service fails to register, start, or unregister during MSI installation, run the installer with logging enabled:
Search
msiexec /i MySetup.msi /l*v install.loginstall.logforCAQuietExecorservy-clioutput to inspect the underlying exit code. -
Review Servy Logs:
servy-cliwrites its own log to%ProgramData%\Servy\logs\Servy.CLI.log, the host service to%ProgramData%\Servy\logs\Servy.Host.log, and the wrapper of your service to%ProgramData%\Servy\logs\services\<ServiceName>\Servy.Service.log. Read them from an elevated prompt: thelogsfolder is restricted to administrators andSYSTEM.
Copyright © Akram El Assas. All rights reserved.
Getting Started
Core Suite
Configuration & Features
- Advanced Configuration
- Logging & Log Rotation
- Health Monitoring & Recovery
- Environment Variables
- Service Dependencies
- Export / Import Services
Lifecycle Hooks
Automation & Production
- Servy Automation & CI/CD
- Embedding in Custom Installers
- Integration with Monitoring Tools
- Service Event Notifications
- Backup / Restore & VM Cloning
Reference & Support