Skip to content

Instantly share code, notes, and snippets.

@sahps
Last active November 20, 2025 16:46
Show Gist options
  • Select an option

  • Save sahps/37fcfb1edc5241a879a0a7df4275cace to your computer and use it in GitHub Desktop.

Select an option

Save sahps/37fcfb1edc5241a879a0a7df4275cace to your computer and use it in GitHub Desktop.
Auto-update Netbird using PowerSHell
#########################################################################
[cmdletbinding()]
param (
[Parameter(Mandatory)]
[string]$NetbirdManagementUrl,
[Parameter()]
[version]$ForcedMinimumVersion = "0.48.0",
[Parameter()]
[string]$NetbirdUrlScheme = "https",
[Parameter()]
[int]$NetbirdUiStartup = 1,
[Parameter()]
[string]$WorkingFolderName = "_Intune"
)
#########################################################################
# Name of package, used in log file name
$packageName = 'Netbird-Check'
#########################################################################
# working directory
$workingDir = ('{0}\{1}' -f $env:ProgramData, $WorkingFolderName)
$workingTimeStamp = (Get-Date -UFormat '%Y%m%d-%H%M%S')
# log files
$logFolder = ('{0}\Logs' -f $workingDir)
$logFileTemplate = '{0}_{1}.txt'
$monthTimestamp = (Get-Date -UFormat '%Y%m') # this script runs hourly, so lets keep standard logs to 1 file per month
$logFileName = ($logFileTemplate -f $packageName, $monthTimestamp)
$logFilePath = ('{0}\{1}' -f $logFolder, $logFileName)
# default var
$isError = $false
# start transcript
[void](New-Item -ItemType Directory $logFolder -ErrorAction SilentlyContinue)
Start-Transcript -Path $logFilePath -Force -Verbose -Append
#########################################################################
# service details
$serviceName = ("Netbird")
# download details
$downloadFolder = ('{0}\Downloads' -f $workingDir)
# we get our updates direct from github
$releasesUrl = 'https://api.github.com/repos/netbirdio/netbird/releases'
# we only download releases that are at least this old (3 days)
$minimumAge = 86400 * 3
# once we find an eligible release, we download the first asset that matches this
$assetMatch = 'netbird*_windows_amd64.msi'
# we download the installer to thie path
$downloadFileName = ("Netbird.Client_{0}.msi" -f $workingTimeStamp)
$downloadPath = ('{0}\{1}' -f $downloadFolder, $downloadFileName)
# the msi log name
$msiLogFileTemplate = '{0}\{1}_{2}_msi_{3}.txt'
# netbird config file
$netbirdConfigPath = ('{0}\Netbird\config.json' -f $env:ProgramData)
# netbird executable
$netbirdExePath = "C:\Program Files\Netbird\netbird.exe"
# netbird ui executable
$netbirdUiExePath = "C:\Program Files\Netbird\Netbird-ui.exe"
# we populate this with the latest release data
$latestRelease = {}
# auto startup of ui
$regKeys = @{
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' = @{
Name = "netbird-ui"
ActionType = "Set"
ActionValue = @{
Type = "STRING"
Value = $netbirdUiExePath
}
}
}
try {
Function Write-Log {
[cmdletbinding()]
param (
[Parameter (Mandatory = $true, ValueFromPipeline = $true)]
[string[]]$Message,
[Parameter (Mandatory = $false)]
[switch]$IsWarning,
[Parameter (Mandatory = $false)]
[switch]$IsError,
[Parameter (Mandatory = $false)]
[string]$Prefix,
[Parameter (Mandatory = $false)]
[System.Exception]$Exception,
[Parameter (Mandatory = $false)]
[switch]$Throw
)
$timestamp = [DateTime]::Now
$type = 'INFO'
$param = @{}
if (($null -ne $Exception) -or ($true -eq $Throw) -or ($true -eq $IsError)) {
$param['ForegroundColor'] = "Red"
$type = "CRIT"
if ($null -ne $Exception) {
$Message = @($Message) + @(($Exception.Message -split "\n"))
}
} elseif ($true -eq $isWarning) {
$param['ForegroundColor'] = "Magenta"
$type = "WARN"
}
if ($true -eq $Throw) {
Throw @($Message)[0]
}
foreach ($string in $Message) {
Write-Host @param ('[{0} | {1}] {2}{3}' -f $timestamp, $type, $Prefix, $string)
}
}
Function Test-IsAdmin() {
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
Function Invoke-Download {
[cmdletbinding()]
param (
[Parameter (Mandatory = $true)]
[string]$Url,
[Parameter (Mandatory = $true)]
[string]$Path
)
Write-Log "Downloading $Url -> $Path"
# set tls
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webClient = New-Object Net.WebClient
$webClient.DownloadFile($Url, $Path)
if (!(Test-Path($Path))) {
Write-Log -Throw "Download failed, file does not exist: $Path"
} elseif ((Get-Item $Path).Length -eq 0) {
Write-Log -Throw "Download failed, file zero length: $Path"
}
}
Function Remove-ItemRepeated {
param (
[Parameter (Mandatory = $true)]
[string]$Path,
[Parameter (Mandatory = $true)]
[int]$Attempts
)
$n = $Attempts
While (Test-Path($Path)) {
if ($n -le 0) { break; }
Remove-Item -Path $Path -Verbose -Force
Start-Sleep 1
$n--
}
if (Test-Path($Path)) { Write-Log -IsWarning "Failed to remove item after $Attempts attempts: $Path" }
}
Function Invoke-PruneFolder {
[cmdletbinding()]
param (
[Parameter (Mandatory = $true)]
[string]$Path,
[Parameter (Mandatory = $false)]
[string]$FileMatch = '*',
[Parameter (Mandatory = $false)]
[int]$DaysToKeep = 30
)
Get-ChildItem -Path $logFolder -Filter $FileMatch -File | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(($DaysToKeep * -1)) } | Remove-Item -Force -Verbose
}
Function Get-App-Version-From-Tag($tag_name) {
# get the latest release tag_name (v8.5.2)
if (($null -eq $tag_name) -or (-not ($tag_name))) {
Write-Log -IsError "Failed to get latest tag_name from parsed Json" -Throw
}
# remove any non-version characters and cast to version
try {
return [version]($tag_name -Replace '[^0-9\.]')
} catch {
Write-Log -IsError "Failed to get parse version from tag_name from parsed Json" -Throw
}
}
Function Get-App-Github-LatestRelease($url, $minimumAge = 0) {
Write-Log "Checking for latest releases from url: $url"
if ($minimumAge -gt 0) {
Write-Log "Rejecting releases that were less than $(("{0:dd}d {0:hh}h {0:mm}m {0:ss}s" -f (New-TimeSpan -Seconds $minimumAge))) seconds old before a new release was published"
}
# fetch json from github api
try {
$releasesJson = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop
} catch {
Write-Log -IsError "Failed to download releases data from url: $url" -Exception $_.Exception
}
# parse content from json
try {
$releases = $releasesJson.Content | ConvertFrom-Json -ErrorAction Stop
} catch {
Write-Log -IsError "Failed to parse json data from url: $url" -Exception $_.Exception
}
try {
# we want the first release that had an age of >minimumAge before the next release was published
$dateOfLastRelease = (Get-Date) # initial test, check against today
foreach ($release in $releases) {
# get the latest release tag_name (v8.5.2)
$releaseTag = $release.tag_name
if (($null -eq $releaseTag) -or (-not ($releaseTag))) {
Write-Log -IsError "Failed to get latest tag_name from parsed Json: $url"
continue
}
try {
$releaseVersion = Get-App-Version-From-Tag -tag_name $releaseTag
} catch {
Write-Log -IsError "Failed to get parse version from tag_name from parsed Json: $url"
continue
}
# how old was this release before the next one was released (or just simply how is the release if its the first one)
$timeSinceNextRelease = (New-TimeSpan -Start $release.published_at -End $dateOfLastRelease)
# does this release meet the age criteria?
if ($timeSinceNextRelease.TotalSeconds -gt $minimumAge) {
Write-Log "Found release: $releaseVersion published at $($release.published_at) [$("{0:dd}d {0:hh}h {0:mm}m {0:ss}s" -f $timeSinceNextRelease)]"
break
} elseif ($releaseVersion -eq $ForcedMinimumVersion) {
Write-Log -IsWarning "Forced minimum release: $releaseVersion published at $($release.published_at) [$("{0:dd}d {0:hh}h {0:mm}m {0:ss}s" -f $timeSinceNextRelease)]"
break
} else {
Write-Log -IsWarning "Reject $releaseVersion published at $($release.published_at) [$("{0:dd}d {0:hh}h {0:mm}m {0:ss}s" -f $timeSinceNextRelease)]"
}
$dateOfLastRelease = $release.published_at
}
} catch {
Write-Log -IsError "Failed to determine appropriate release from github releases: $url" -Exception $_.Exception
}
# fetch the asset of this latest release that matches the 64-bit exe installer
$latestAsset = $release.assets |Where-Object { $_.Name -like $assetMatch }
if (($null -eq $latestAsset) -or (-not ($latestAsset))) {
Write-Log -IsError "Failed to get latest asset (matching '$assetMatch') from parsed Json: $url" -Throw
}
# fetch the download url from matching asset
$downloadUrl = $latestAsset.browser_download_url
if (($null -eq $downloadUrl) -or (-not ($downloadUrl))) {
Write-Log -IsError "Failed to get download url from latest asset: $url" -Throw
}
# return latest info
return @{
"Version" = $releaseVersion
"Url" = $downloadUrl
}
}
Function Invoke-Download-and-Install-MSI-App {
# create download folder
if (!(Test-Path $downloadFolder)) {
New-Item -Type Directory -Path $downloadFolder -ErrorAction Stop
}
# get latest release from previously populated variable
$downloadUrl = ($script:latestRelease)['Url']
# download package
Invoke-Download -Url $downloadUrl -Path $downloadPath
$msiCount = 0
try {
$msiCount++
$params = @{
FilePath = "msiexec"
ArgumentList = @(
"/i",
"`"$downloadPath`"",
"/qn",
"/norestart",
"/l*v",
"`"$($msiLogFileTemplate -f $logFolder, $packageName, $workingTimeStamp, $msiCount)`"",
"REBOOT=ReallySuppress"
)
Wait = $true
Passthru = $true
}
Write-Log "Installing app: $downloadPath $($params['ArgumentList'] -Join ' ')"
$params | ConvertTo-Json | Write-Log
$p = Start-Process @params
Write-Log "Installation completed. Exit code: $($p.ExitCode)"
} catch {
Write-Log -IsError "Failed to install"
Throw
}
Remove-ItemRepeated -Path $downloadPath -Attempts 10
}
Function Test-And-Configure-Netbird() {
if (-not (Test-Path $netbirdConfigPath)) {
Write-Log -Throw "Unable to find netbird configuration file: $netbirdConfigPath"
}
try {
Write-Log "Fetching Netbird config as json: $netbirdConfigPath"
$configJson = Get-Content -Raw -Path $netbirdConfigPath
} catch {
Write-Log -IsError "Unable to read config file: $netbirdConfigPath" -Exception $_.Exception
}
try {
$configData = ($configJson | ConvertFrom-Json)
} catch {
Write-Log -IsError "Unable to parse config file: $netbirdConfigPath" -Exception $_.Exception
}
try {
if (
($configData.ManagementURL.Host -ne $NetbirdManagementUrl) -or
($configData.ManagementURL.Scheme -ne $NetbirdUrlScheme)
) {
Write-Log "Netbird configuration needs updating"
# adjust config
# management
$configData.ManagementURL.Scheme = $NetbirdUrlScheme
$configData.ManagementURL.Host = $NetbirdManagementUrl
# create JSON from config
$updatedConfigJson = $configData | ConvertTo-Json -Depth 10
# save config
Write-Log "Writing to netbird config"
Set-Content -Path $netbirdConfigPath -Value $updatedConfigJson
} else {
Write-Log "Netbird configuration meets requirements"
Write-Log " ManagementURL.Scheme = $($configData.ManagementURL.Scheme)"
Write-Log " ManagementURL.Host = $($configData.ManagementURL.Host)"
}
} catch {
Write-Log -IsError "Failed to update config file: $netbirdConfigPath" -Exception $_.Exception
}
}
Function Test-App-Service {
try {
Write-Log "Fetching service details: $serviceName"
$service = Get-Service -Name $serviceName -ErrorAction Stop
} catch {
Write-Log -IsWarning "Service [$serviceName] not found"
return "missing"
}
if (!$service) { Write-Log "Failed to get service object: $serviceName" -Throw }
try {
if ($service.StartType -ne "Automatic") {
Write-Log "Setting service [$serviceName] to Automatic"
$service = ($service | Set-Service -StartupType Automatic -PassThru -ErrorAction Stop)
}
} catch {
Write-Log "Failed setting service to Automatic" -Exception $_.Exception -Throw
}
try {
if ($service.Status -ne "Running") {
Write-Log "Starting service [$serviceName]"
$service = ($service | Start-Service -PassThru -Verbose -ErrorAction Stop)
}
} catch {
Write-Log "Failed starting service" -Exception $_.Exception -Throw
}
Write-Log ("Service [{0}]: {1} ({2})" -f $service.Name, $service.Status, $service.StartType)
if ($service.StartType -ne "Automatic") {
Write-Log -IsWarning "Failed to set service [$serviceName] to Automatic"
}
if ($service.Status -ne "Running") {
Write-Log -IsWarning "Failed to start service [$serviceName]"
}
}
Function Get-App-CurrentVersion($path) {
if (-not (Test-Path $path)) {
Write-Log -IsWarning "Application executable not found: $path"
return [version]"0.0.0.0"
}
try {
$version = (Get-Item $path).VersionInfo.FileVersionRaw
Write-Log "Current version: $($version.ToString())"
return (Get-Item $path).VersionInfo.FileVersionRaw
} catch {
Write-Log -IsError "Unable to get version from file: $path" -Exception $_.Exception
}
}
Function Test-App-UpdateAvailable() {
$script:latestRelease = Get-App-Github-LatestRelease -url $releasesUrl -minimumAge $minimumAge
$currentVersion = Get-App-CurrentVersion -path $netbirdExePath
# check if we received data
if (($script:latestRelease.Keys -contains 'Url') -and ($script:latestRelease.Keys -contains 'Version') -and ($script:latestRelease['Version'].Length -gt 0) -and ($script:latestRelease['Url'].Length -gt 0)) {
if ($currentVersion -lt $script:latestRelease['Version']) {
Write-Log "New update available"
return $true
} else {
Write-Log "No update required"
}
} else {
Write-Log -IsWarning "Failed to fetch data from github for latest release. Unable to check for updates."
}
return $false
}
Function Test-And-Promote-App-In-SystemTray($appFileName) {
foreach ($user in (Get-ChildItem Registry::HKEY_USERS -ErrorAction SilentlyContinue)) {
# filter the items we don't need
if ($user.Name -notmatch '^.*\\S(-[0-9]+){7}$') { continue }
# we are now left with real user SIDs
$NotifyIconSettings = @(Get-ChildItem -Path ('{0}\Control Panel\NotifyIconSettings' -f $user.PSPath) -ErrorAction SilentlyContinue)
if ($NotifyIconSettings.Count -eq 0) { Write-Log -IsWarning "Registry hive for SID [$($user.Name)] is missing data for NotifyIconSettings" }
$found = $false
foreach ($app in $NotifyIconSettings) {
$appProp = Get-ItemProperty $app.PSPath
if (($null -ne $appProp.ExecutablePath) -and ($appProp.ExecutablePath -like ('*\{0}' -f $appFileName))) {
if ($appProp.IsPromoted -ne 1) {
Write-Log "System tray icon being promoted: $appFileName"
Set-RegValue -regKey $app.PSPath -regName 'IsPromoted' -regValue 1 -regType 'DWORD'
} else {
Write-Log "System tray icon already promoted: $appFileName"
}
$found = $true
}
}
if ($false -eq $found) { Write-Log -IsWarning "Failed to find matching system tray app: $appFileName" }
}
}
Function Set-RegValue($regKey, $regName, $regValue, $regType) {
try {
Write-Log ("Setting registry value: {0}: {1} = {2} [{3}]" -f $regKey, $regName, $regValue, $regType)
Set-ItemProperty -Path $regKey -Name $regName -Value $regValue -Type $regType -ErrorAction Stop
} catch {
Write-Log -Throw "Failed to set registry value" -Exception $_.Exception
}
}
Function Update-RegValues($regKeys) {
foreach ($regKey in $regKeys.Keys) {
$regCfg = $regKeys[$regKey]
if ($regCfg.ContainsKey("Name")) {
$regName = $regCfg["Name"]
$regCurrentValue = (Get-ItemProperty -Path $regKey -Name $regName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $regName)
}
Switch ($regCfg["ActionType"]) {
"Set" {
$regType = $regCfg["ActionValue"]["Type"]
$regValue = $regCfg["ActionValue"]["Value"]
if ($regCurrentValue -ne $regValue) {
if (-not(Test-Path -Path $regKey -PathType Container)) {
try {
Write-Log "Creating registry key: $regKey"
New-Item -Path $regKey -ItemType Container -Force -ErrorAction Stop
} catch {
Write-Log -Throw "Failed to create registry key" -Exception $_.Exception
}
}
Set-RegValue -regKey $regKey -regName $regName -regValue $regValue -regType $regType
}
}
"Replace" {
$regReplace = $regCfg["ActionValue"]["Replace"]
$regWith = $regCfg["ActionValue"]["With"]
if ($regCurrentValue -like "*$($regReplace)*") {
$regType = (Get-Item -Path $regKey).GetValueKind($regName)
$regValue = ($regCurrentValue -Replace $regReplace, $regWith)
Set-RegValue -regKey $regKey -regName $regName -regValue $regValue -regType $regType
}
}
"DeleteProperty" {
if ((Test-Path $regKey) -and ((Get-Item $regKey | Select-Object -ExpandProperty Property) -contains $regName)) {
Write-Log "Deleting property: $regKey | $regName"
Remove-ItemProperty -Force -Path $regKey -Name $regName -Verbose
} else {
Write-Log "Property doesn't exist, nothing to do: $regKey | $regName"
}
}
"DeleteKey" {
if (Test-Path $regKey) {
Write-Log "Deleting key: $regName"
Remove-Item -Recurse -Force -Path $regKey -Verbose
} else {
Write-Log "Key doesn't exist, nothing to do: $regKey"
}
}
}
}
}
Function Invoke-Scriptblock-With-Timeout($ScriptBlock, $ArgumentList = @(), $Timeout = 0) {
if ($Timeout -le 0) { $Timeout = 10 }
$jobExpires = (Get-Date).AddSeconds($Timeout)
$job = Start-Job -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList
while (((Get-Date) -lt $jobExpires) -and ((Get-Job -Id $job.Id).State -ne 'Completed')) {
Start-Sleep -Milliseconds 250
}
$result = @{ 'completed' = ((Get-Job -Id $job.Id).State -eq 'Completed'); 'output' = @($job | Receive-Job) }
Stop-Job $job -PassThru | Remove-Job
return $result
}
Function Test-App-Response() {
Write-Log "Fetching output from 'netbird status'.."
$result = Invoke-Scriptblock-With-Timeout -ScriptBlock { netbird status }
if ($true -ne $result.completed) {
Write-Log "Timeout from 'netbird status'; Stopping service.."
Get-Process -Name 'netbird' | Stop-Process -Force -Verbose
Stop-Service 'netbird' -Force -Verbose -ErrorAction SilentlyContinue
Test-App-Service
} else {
Write-Log " === Netbird service status ==="
Write-Log -Prefix " " -Message @($result.output)
Write-Log " =============================="
}
}
if (-not (Test-IsAdmin)) {
Write-Log -IsError "This script needs to be run as administrator." -Throw
}
Invoke-PruneFolder -Path $logFolder
# disable progress bar to significantly speed up Invoke-WebRequest
# https://stackoverflow.com/questions/28682642/powershell-why-is-using-invoke-webrequest-much-slower-than-a-browser-download
$ProgressPreference = 'SilentlyContinue'
# check for updates; this will also install netbird if the executable is missing
if (Test-App-UpdateAvailable) { Invoke-Download-and-Install-MSI-App }
# check the service is in good order; this will also install netbird if the service is missing
if ((Test-App-Service) -eq "missing") {
if (Test-Path $netbirdExePath) {
Write-Log "Installing Netbird service via executable: $($netbirdExePath) service install"
& $netbirdExePath service install
Test-App-Service
} else {
Invoke-Download-and-Install-MSI-App
}
}
# check netbird is responsive
Test-App-Response
# check and fix config
Test-And-Configure-Netbird
if ($NetbirdUiStartup -eq 1) {
# check the UI is configured to start on logon
Update-RegValues -regKeys $regKeys
# Ensure the netbird app isn't hidden in system tray
Test-And-Promote-App-In-SystemTray -appFileName 'netbird-ui.exe'
}
} catch {
$isError = $true
Write-Log "Exception thrown" -Exception $_.Exception
Throw
} finally {
if ($isError) {
Write-Log -IsWarning "Completed with errors."
}
Stop-Transcript -Verbose
}
@sahps
Copy link
Author

sahps commented Jun 26, 2025

This is the code I am using to keep Windows clients up to date. It is based on a template that I use for all Intune scripts. The script is distributed using an Intune win32-app which schedules this script to run hourly as the SYSTEM user.

The script will

  • Check the service is installed
    • Will run netbird service install if Netbird is installed
    • Will install Netbird if it is not
  • Check the service is running and set Automatic
    • Will start the service and/or set to Automatic if not
  • Check for updates using api.github.com
    • Will only download an update if it was not superceded by another update within 3 days. This reduces the chance of downloading a buggy update by assuming the developers would release a new update within 3 days if it was a critical bug.
    • Supports pinning a minimum version
  • Sets the netbird-ui.exe app to run on startup for all users
  • Will promote the netbird-ui.exe app on the system tray so it is not hidden for any logged in user
  • Logs all efforts to C:\ProgramData\_Intune\Logs
    • Supports changing "_Intune" to anything else
    • Keeps the log files trimmed, by default to last 30 days
CoPilot summary

🛠️ Purpose

This script is designed to automate the update and configuration of Netbird, a secure networking tool, on Windows systems. It checks for updates from GitHub, installs them if needed, ensures the service is running, configures the application, and sets up the UI to start automatically.


🔍 Key Features & Workflow

1. Parameters

The script accepts several parameters:

  • NetbirdManagementUrl, NetbirdAdminUrl: URLs for Netbird configuration.
  • ForcedMinimumVersion: Minimum version to enforce.
  • NetbirdUrlScheme: URL scheme (default is https).
  • NetbirdUiStartup: Whether to auto-start the UI.
  • WorkingFolderName: Folder name for logs and downloads.

2. Logging Setup

  • Creates a working directory and log folder.
  • Logs are timestamped and stored monthly.
  • Uses Start-Transcript to capture all output.

3. Functions Defined

The script defines many helper functions:

  • Write-Log: Custom logging with severity levels.
  • Test-IsAdmin: Checks if the script is run as administrator.
  • Invoke-Download: Downloads files from URLs.
  • Remove-ItemRepeated: Tries to delete a file multiple times.
  • Invoke-PruneFolder: Deletes old log files.
  • Get-App-Github-LatestRelease: Fetches the latest Netbird release from GitHub.
  • Invoke-Download-and-Install-MSI-App: Downloads and installs the MSI package.
  • Test-And-Configure-Netbird: Validates and updates the Netbird config file.
  • Test-App-Service: Ensures the Netbird service is running and set to auto-start.
  • Get-App-CurrentVersion: Gets the installed Netbird version.
  • Test-App-UpdateAvailable: Checks if an update is available.
  • Test-And-Promote-App-In-SystemTray: Promotes the Netbird UI icon in the system tray.
  • Set-RegValue & Update-RegValues: Registry manipulation for auto-start and tray visibility.

4. Main Execution Flow

  • Checks for admin privileges.
  • Prunes old logs.
  • Checks for updates and installs if needed.
  • Ensures the Netbird service is installed and running.
  • Validates and updates configuration.
  • Sets up UI to auto-start and promotes its tray icon.

✅ Summary

This script is a robust automation tool for managing Netbird on Windows. It ensures:

  • The latest version is installed.
  • The service is running.
  • Configuration is correct.
  • The UI starts with Windows and is visible in the system tray.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment