fix: Windows service via NSSM (sc.exe incompatible)
CI / test (push) Successful in 2m22s

Error 1053 — sc.exe alone cannot run non-service-aware .exe.
ntfy-bridge needs NSSM (Non-Sucking Service Manager) wrapper.

- service.v: --install-service uses nssm.exe (auto-detect PATH or dir)
- scripts/install.ps1: auto-downloads NSSM 2.24 before installing
- Falls back to sc.exe for uninstall/status if nssm not found
This commit is contained in:
2026-08-03 17:04:12 -04:00
parent 45e349e79d
commit 4129379ca5
2 changed files with 147 additions and 28 deletions
+38 -10
View File
@@ -16,25 +16,50 @@ Write-Host ""
$v = Get-Command v -ErrorAction SilentlyContinue
if (-not $v) {
Write-Host "V compiler not found. Downloading..."
# Download V weekly build
$vUrl = "https://github.com/vlang/v/releases/download/weekly.2024.32/v_windows.zip"
$vZip = "$env:TEMP\v_windows.zip"
$vDir = "$env:LOCALAPPDATA\v"
Invoke-WebRequest -Uri $vUrl -OutFile $vZip
Expand-Archive -Path $vZip -DestinationPath $vDir -Force
Remove-Item $vZip
$env:PATH = "$vDir\v;$env:PATH"
Write-Host "V installed to $vDir"
}
# Install NSSM (required for Windows service)
$nssmPath = "$env:ProgramFiles\ntfy-bridge\nssm.exe"
if (-not (Test-Path $nssmPath)) {
Write-Host "Downloading NSSM (Non-Sucking Service Manager)..."
$nssmUrl = "https://nssm.cc/release/nssm-2.24.zip"
$nssmZip = "$env:TEMP\nssm.zip"
$nssmDir = "$env:TEMP\nssm_extract"
Invoke-WebRequest -Uri $nssmUrl -OutFile $nssmZip
Expand-Archive -Path $nssmZip -DestinationPath $nssmDir -Force
# Find nssm.exe in the extracted folder (it's in a win64 subfolder)
$nssmExe = Get-ChildItem -Path $nssmDir -Recurse -Name "nssm.exe" | Select-Object -First 1
if (-not $nssmExe) {
Write-Error "Could not find nssm.exe in the downloaded archive."
exit 1
}
$extractedNssm = Join-Path $nssmDir $nssmExe
New-Item -ItemType Directory -Force -Path "$env:ProgramFiles\ntfy-bridge" | Out-Null
Copy-Item -Force $extractedNssm $nssmPath
Remove-Item $nssmZip
Remove-Item -Recurse -Force $nssmDir
Write-Host "NSSM installed to $nssmPath"
}
# Build
Write-Host "Building ntfy-bridge..."
v -prod -o ntfy-bridge.exe .
# Copy to a permanent location
# Copy to install directory
$installDir = "$env:ProgramFiles\ntfy-bridge"
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
Copy-Item -Force ntfy-bridge.exe $installDir\
@@ -47,9 +72,9 @@ if (-not (Test-Path "$installDir\ntfy-bridge.yaml")) {
Write-Host " ⚠️ Edit $installDir\ntfy-bridge.yaml to configure your sources!" -ForegroundColor Yellow
}
# Install as Windows service
# Install as Windows service (via NSSM)
Write-Host "Installing as Windows service..."
& "$installDir\ntfy-bridge.exe" --install-service --config "$installDir\ntfy-bridge.yaml" 2>&1 | Out-Null
& "$installDir\ntfy-bridge.exe" --install-service --config "$installDir\ntfy-bridge.yaml" 2>&1 | Write-Host
Write-Host ""
Write-Host "Done! ntfy-bridge is installed and running as a Windows service." -ForegroundColor Green
@@ -60,6 +85,9 @@ Write-Host " Dashboard: http://localhost:9090/"
Write-Host " Health: http://localhost:9090/health"
Write-Host ""
Write-Host " Service commands:"
Write-Host " sc query ntfy-bridge"
Write-Host " sc stop ntfy-bridge"
Write-Host " sc start ntfy-bridge"
Write-Host " nssm status ntfy-bridge"
Write-Host " nssm stop ntfy-bridge"
Write-Host " nssm start ntfy-bridge"
Write-Host " nssm remove ntfy-bridge confirm (uninstall)"
Write-Host ""
Write-Host " Logs: C:\ProgramData\ntfy-bridge\service.log"
+109 -18
View File
@@ -9,22 +9,72 @@ pub fn service_install(config_path string, quiet bool) ! {
println(' Config: ${abs_config}')
$if windows {
args := '--quiet --config "${abs_config}"'
name := 'ntfy-bridge'
cmd := 'sc create ${name} binPath= "${exe_path} ${args}" start= auto DisplayName= "ntfy-bridge Notification Hub"'
r := os.execute(cmd)
if r.exit_code != 0 {
eprintln('sc create failed: ${r.output}')
args := '--quiet --config "${abs_config}"'
// Find nssm
mut nssm_exe := ''
dir := os.dir(exe_path)
candidate := os.join_path(dir, 'nssm.exe')
if os.exists(candidate) {
nssm_exe = candidate
} else {
r := os.execute('where nssm 2>nul')
if r.exit_code == 0 && r.output.contains('nssm') {
nssm_exe = 'nssm'
}
}
if nssm_exe == '' {
eprintln('nssm.exe not found. Windows requires NSSM to run ntfy-bridge as a service.')
eprintln('')
eprintln('Install NSSM (Non-Sucking Service Manager):')
eprintln(' scoop install nssm')
eprintln(' OR download from https://nssm.cc/download')
eprintln(' OR place nssm.exe next to ntfy-bridge.exe')
exit(1)
}
println(' Service created.')
os.execute('sc failure ${name} reset= 86400 actions= restart/10000/restart/30000/restart/60000')
os.execute('sc description ${name} "Homelab notification hub"')
r2 := os.execute('sc start ${name}')
if r2.exit_code != 0 {
println(' Warning: sc start ${name} manually.')
println(' Using nssm: ${nssm_exe}')
// Remove previous install if exists
os.execute('"${nssm_exe}" stop ${name}')
os.execute('"${nssm_exe}" remove ${name} confirm')
// Install the service
r := os.execute('"${nssm_exe}" install ${name} "${exe_path}" ${args}')
if r.exit_code != 0 {
eprintln('nssm install failed: ${r.output}')
exit(1)
}
println(' Service started. sc start|stop|query|delete ${name}')
println(' Service registered.')
// Configure service
os.execute('"${nssm_exe}" set ${name} AppStdout C:\\ProgramData\\ntfy-bridge\\service.log')
os.execute('"${nssm_exe}" set ${name} AppStderr C:\\ProgramData\\ntfy-bridge\\service.log')
os.execute('"${nssm_exe}" set ${name} AppRotateFiles 1')
os.execute('"${nssm_exe}" set ${name} AppRotateSeconds 86400')
os.execute('"${nssm_exe}" set ${name} AppRotateBytes 1048576')
os.execute('"${nssm_exe}" set ${name} DisplayName "ntfy-bridge Notification Hub"')
os.execute('"${nssm_exe}" set ${name} Description "Homelab notification hub"')
os.execute('"${nssm_exe}" set ${name} Start SERVICE_AUTO_START')
// Create log directory
os.mkdir_all('C:\\ProgramData\\ntfy-bridge') or {}
// Start the service
r2 := os.execute('"${nssm_exe}" start ${name}')
if r2.exit_code != 0 {
println(' Warning: nssm start ${name} failed — start manually.')
} else {
println(' Service started successfully.')
}
println('')
println('Service commands:')
println(' nssm status ${name}')
println(' nssm start ${name}')
println(' nssm stop ${name}')
println(' nssm restart ${name}')
println(' nssm remove ${name} confirm (uninstall)')
return
}
$if linux {
@@ -102,10 +152,32 @@ pub fn service_uninstall() ! {
$if windows {
name := 'ntfy-bridge'
os.execute('sc stop ${name}')
r := os.execute('sc delete ${name}')
if r.exit_code != 0 {
return error('sc delete failed: ${r.output}')
// Find nssm
exe_path := os.executable()
mut nssm_exe := ''
dir := os.dir(exe_path)
candidate := os.join_path(dir, 'nssm.exe')
if os.exists(candidate) {
nssm_exe = candidate
} else {
r := os.execute('where nssm 2>nul')
if r.exit_code == 0 && r.output.contains('nssm') {
nssm_exe = 'nssm'
}
}
if nssm_exe != '' {
os.execute('"${nssm_exe}" stop ${name}')
r := os.execute('"${nssm_exe}" remove ${name} confirm')
if r.exit_code != 0 {
return error('nssm remove failed: ${r.output}')
}
} else {
os.execute('sc stop ${name}')
r := os.execute('sc delete ${name}')
if r.exit_code != 0 {
return error('sc delete failed: ${r.output}')
}
}
println(' Service uninstalled.')
return
@@ -142,8 +214,27 @@ pub fn service_uninstall() ! {
pub fn service_status() ! {
$if windows {
r := os.execute('sc query ntfy-bridge')
if r.exit_code == 0 { println(r.output) } else { println('Not installed.') }
// Find nssm
exe_path := os.executable()
mut nssm_exe := ''
dir := os.dir(exe_path)
candidate := os.join_path(dir, 'nssm.exe')
if os.exists(candidate) {
nssm_exe = candidate
} else {
r := os.execute('where nssm 2>nul')
if r.exit_code == 0 && r.output.contains('nssm') {
nssm_exe = 'nssm'
}
}
if nssm_exe != '' {
r := os.execute('"${nssm_exe}" status ntfy-bridge')
if r.exit_code == 0 { println(r.output) } else { println('Not installed.') }
} else {
r := os.execute('sc query ntfy-bridge')
if r.exit_code == 0 { println(r.output) } else { println('Not installed.') }
}
return
}
$if linux {