module main import os import time pub fn service_install(config_path string, quiet bool) ! { exe_path := os.executable() abs_config := os.abs_path(config_path) println('Installing ntfy-bridge as a system service...') println(' Config: ${abs_config}') $if windows { name := 'ntfy-bridge' 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(' Using nssm: ${nssm_exe}') // Clean up previous nssm install (NOT sc.exe — sc.exe can cause "marked for deletion") os.execute('"${nssm_exe}" stop ${name} 2>nul') os.execute('"${nssm_exe}" remove ${name} confirm 2>nul') // Wait for SCM to release time.sleep(2 * time.second) // Install the service (retry up to 3 times for "marked for deletion") mut installed := false for attempt := 1; attempt <= 3; attempt++ { r := os.execute('"${nssm_exe}" install ${name} "${exe_path}" ${args}') if r.exit_code == 0 { installed = true break } if r.output.contains('marked for deletion') { println(' Service still marked for deletion, waiting...') time.sleep(3 * time.second) continue } eprintln('nssm install failed: ${r.output}') exit(1) } if !installed { eprintln('nssm install failed after 3 attempts. Close services.msc and try again.') exit(1) } 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 { // Detect init system is_systemd := os.exists('/run/systemd/system') || os.exists('/sys/fs/cgroup/systemd') is_openrc := os.exists('/etc/init.d') && (os.exists('/sbin/openrc') || os.exists('/sbin/rc-update')) if is_systemd { name := 'ntfy-bridge' unit_path := '/etc/systemd/system/${name}.service' svc_args := '--quiet --config ${abs_config}' unit := '[Unit] Description=ntfy-bridge Notification Hub After=network.target [Service] Type=simple ExecStart=${exe_path} ${svc_args} Restart=always RestartSec=10 StandardOutput=journal StandardError=journal SyslogIdentifier=ntfy-bridge [Install] WantedBy=multi-user.target ' println(' Writing ${unit_path}') os.write_file(unit_path, unit) or { return error('Failed to write ${unit_path} (sudo?).') } os.execute('systemctl daemon-reload') os.execute('systemctl enable ${name}') r := os.execute('systemctl start ${name}') println(' Service installed. systemctl status|stop|start ${name}') if r.exit_code != 0 { println(' Warning: start failed.') } return } if is_openrc { name := 'ntfy-bridge' init_path := '/etc/init.d/${name}' svc_args := '--quiet --config ${abs_config}' script := "#!/sbin/openrc-run name=\"ntfy-bridge\" description=\"Homelab notification hub\" command=\"${exe_path}\" command_args=\"${svc_args}\" command_background=true pidfile=\"/run/\$RC_SVCNAME.pid\" depend() { \tneed net \tafter firewall } " println(' Writing ${init_path}') os.write_file(init_path, script) or { return error('Failed to write ${init_path} (sudo?).') } os.execute('chmod +x ${init_path}') os.execute('rc-update add ${name} default') r := os.execute('rc-service ${name} start') println(' Service installed. rc-service ${name} status|stop|start') if r.exit_code != 0 { println(' Warning: start failed.') } return } return error('Unsupported init system (need systemd or openrc).') } return error('Unsupported OS.') } pub fn service_uninstall() ! { println('Uninstalling ntfy-bridge service...') $if windows { name := 'ntfy-bridge' // 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 } $if linux { is_systemd := os.exists('/run/systemd/system') || os.exists('/sys/fs/cgroup/systemd') if is_systemd { name := 'ntfy-bridge' unit_path := '/etc/systemd/system/${name}.service' os.execute('systemctl stop ${name}') os.execute('systemctl disable ${name}') if os.exists(unit_path) { os.rm(unit_path) or { return error('Failed to remove ${unit_path} (sudo?).') } } os.execute('systemctl daemon-reload') println(' Service uninstalled.') return } if os.exists('/etc/init.d/ntfy-bridge') { os.execute('rc-service ntfy-bridge stop') os.execute('rc-update del ntfy-bridge') os.rm('/etc/init.d/ntfy-bridge') or { return error('Failed to remove /etc/init.d/ntfy-bridge (sudo?).') } println(' Service uninstalled.') return } return error('Service not found.') } return error('Unsupported OS.') } pub fn service_status() ! { $if windows { // 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 { is_systemd := os.exists('/run/systemd/system') || os.exists('/sys/fs/cgroup/systemd') if is_systemd { r := os.execute('systemctl is-active ntfy-bridge') println('Status: ${r.output.trim_space()}') return } if os.exists('/etc/init.d/ntfy-bridge') { r := os.execute('rc-service ntfy-bridge status') println(r.output) return } println('Not installed.') return } println('Service status not supported on this OS.') }