# ========================================================= # MAX Windows Toolkit - 使用 NSSM 管理开机启动服务 # 中文界面,统一风格 # ========================================================= $Common = "C:\Setup\windows\lib\common.ps1" if (Test-Path $Common) { . $Common } Write-Header "使用 NSSM 管理开机启动服务" Show-AdminWarn # --------------------------------------------------------- # 配置 # --------------------------------------------------------- $RepoBase = "https://dl.unvmax.com/windows" $ToolsBaseUrl = "$RepoBase/tools-bin" $LocalRoot = "C:\Setup\windows" $ToolsDir = Join-Path $LocalRoot "tools-bin" $NssmDir = Join-Path $ToolsDir "nssm" $LogDir = "C:\Setup\Logs\nssm" $NssmFileName = "nssm.exe" $NssmUrl = "$ToolsBaseUrl/$NssmFileName" $NssmPath = Join-Path $NssmDir $NssmFileName New-Item -ItemType Directory -Path $ToolsDir -Force | Out-Null New-Item -ItemType Directory -Path $NssmDir -Force | Out-Null New-Item -ItemType Directory -Path $LogDir -Force | Out-Null # --------------------------------------------------------- # 辅助函数 # --------------------------------------------------------- function Ensure-NSSM { if (Test-Path $NssmPath) { return $true } Write-Warn "本地未找到 NSSM" Write-Muted "期望路径: $NssmPath" return Download-OrUpdateNSSM } function Download-OrUpdateNSSM { Write-Section "下载 / 更新 NSSM" Write-Muted "URL : $NssmUrl" Write-Muted "路径: $NssmPath" if (Download-File -Url $NssmUrl -OutFile $NssmPath) { try { Unblock-File -Path $NssmPath -ErrorAction SilentlyContinue } catch {} Write-OK "NSSM 已就绪" return $true } Write-Err "下载 NSSM 失败" return $false } function Invoke-NSSM { param ( [string[]]$Arguments ) if (-not (Ensure-NSSM)) { return $false } try { & $NssmPath @Arguments return $true } catch { Write-Err "NSSM 命令失败" Write-Muted $_.Exception.Message return $false } } function Test-ServiceExists { param ( [string]$ServiceName ) $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue return ($null -ne $svc) } function Normalize-ServiceName { param ( [string]$Name ) $Name = $Name.Trim() $Name = $Name -replace "\s+", "_" $Name = $Name -replace "[^\w\-]", "_" return $Name } function Add-NSSMToSystemPath { Write-Section "将 NSSM 添加到系统 PATH" if (-not (Test-IsAdmin)) { Write-Err "请以管理员身份运行 PowerShell" Write-Warn "添加系统 PATH 需要管理员权限" return } if (-not (Ensure-NSSM)) { return } $NssmFolder = Split-Path $NssmPath -Parent $CurrentPath = [Environment]::GetEnvironmentVariable("Path", "Machine") if ([string]::IsNullOrWhiteSpace($CurrentPath)) { $CurrentPath = "" } $PathItems = $CurrentPath -split ";" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } $AlreadyExists = $false foreach ($Item in $PathItems) { if ($Item.TrimEnd("\") -ieq $NssmFolder.TrimEnd("\")) { $AlreadyExists = $true break } } if ($AlreadyExists) { Write-OK "NSSM 目录已存在于系统 PATH 中:" Write-Muted $NssmFolder Write-Info "" Write-Info "重开 PowerShell 或 CMD 后测试:" Write-Muted "nssm version" Write-Muted "where nssm" return } try { $NewPath = $CurrentPath.TrimEnd(";") if ([string]::IsNullOrWhiteSpace($NewPath)) { $NewPath = $NssmFolder } else { $NewPath = "$NewPath;$NssmFolder" } [Environment]::SetEnvironmentVariable("Path", $NewPath, "Machine") # 同时更新当前进程 PATH,使本会话能立即使用 nssm if ($env:Path -notlike "*$NssmFolder*") { $env:Path = "$env:Path;$NssmFolder" } Write-OK "NSSM 目录已添加到系统 PATH:" Write-Muted $NssmFolder Write-Info "" Write-OK "当前会话的 PATH 也已更新" Write-Info "" Write-Info "现在可以测试:" Write-Muted "nssm version" Write-Muted "where nssm" Write-Info "" Write-Warn "如果已打开其他终端,请重开 PowerShell 或 CMD 以重新加载 PATH" } catch { Write-Err "添加 NSSM 到系统 PATH 失败" Write-Muted $_.Exception.Message } } function Install-CustomExeService { Write-Section "将 EXE 安装为开机启动服务" if (-not (Test-IsAdmin)) { Write-Err "请以管理员身份运行 PowerShell" return } if (-not (Ensure-NSSM)) { return } $ServiceName = Read-Host "服务名称,如 frpc" $ServiceName = Normalize-ServiceName $ServiceName if ([string]::IsNullOrWhiteSpace($ServiceName)) { Write-Err "服务名称不能为空" return } if (Test-ServiceExists -ServiceName $ServiceName) { Write-Warn "服务已存在: $ServiceName" return } $ExePath = Read-Host "EXE 完整路径,如 C:\frp\frpc.exe" if (-not (Test-Path $ExePath)) { Write-Err "EXE 文件未找到: $ExePath" return } $Arguments = Read-Host "参数,如 -c C:\frp\frpc.toml" $WorkDir = Read-Host "工作目录,直接回车使用 EXE 所在文件夹" if ([string]::IsNullOrWhiteSpace($WorkDir)) { $WorkDir = Split-Path $ExePath -Parent } if (-not (Test-Path $WorkDir)) { Write-Err "工作目录未找到: $WorkDir" return } $Stdout = Join-Path $LogDir "$ServiceName.out.log" $Stderr = Join-Path $LogDir "$ServiceName.err.log" Write-Info "" Write-Info "服务摘要:" Write-Info "名称 : $ServiceName" Write-Info "EXE : $ExePath" Write-Info "参数 : $Arguments" Write-Info "工作目录 : $WorkDir" Write-Info "标准输出 : $Stdout" Write-Info "错误输出 : $Stderr" Write-Info "" $Confirm = Read-Host "是否安装此服务?输入 YES 确认" if ($Confirm -ne "YES") { Write-Warn "已取消" return } try { & $NssmPath install $ServiceName $ExePath $Arguments & $NssmPath set $ServiceName AppDirectory $WorkDir & $NssmPath set $ServiceName Start SERVICE_AUTO_START & $NssmPath set $ServiceName AppStdout $Stdout & $NssmPath set $ServiceName AppStderr $Stderr & $NssmPath set $ServiceName AppRotateFiles 1 & $NssmPath set $ServiceName AppRotateOnline 1 & $NssmPath set $ServiceName AppRotateBytes 10485760 & $NssmPath set $ServiceName AppRestartDelay 5000 & $NssmPath set $ServiceName AppThrottle 1500 Start-Service -Name $ServiceName -ErrorAction SilentlyContinue Write-Info "" Write-OK "服务已安装并启动: $ServiceName" } catch { Write-Err "安装服务失败" Write-Muted $_.Exception.Message } } function Install-PowerShellScriptService { Write-Section "将 PowerShell 脚本安装为开机启动服务" if (-not (Test-IsAdmin)) { Write-Err "请以管理员身份运行 PowerShell" return } if (-not (Ensure-NSSM)) { return } $ServiceName = Read-Host "服务名称,如 MyWatcher" $ServiceName = Normalize-ServiceName $ServiceName if ([string]::IsNullOrWhiteSpace($ServiceName)) { Write-Err "服务名称不能为空" return } if (Test-ServiceExists -ServiceName $ServiceName) { Write-Warn "服务已存在: $ServiceName" return } $Ps1Path = Read-Host "PowerShell 脚本完整路径,如 C:\Scripts\watcher.ps1" if (-not (Test-Path $Ps1Path)) { Write-Err "脚本文件未找到: $Ps1Path" return } $WorkDir = Split-Path $Ps1Path -Parent $PowerShellExe = "$env:WinDir\System32\WindowsPowerShell\v1.0\powershell.exe" $AppParameters = "-ExecutionPolicy Bypass -NoProfile -File `"$Ps1Path`"" $Stdout = Join-Path $LogDir "$ServiceName.out.log" $Stderr = Join-Path $LogDir "$ServiceName.err.log" Write-Info "" Write-Info "服务摘要:" Write-Info "名称 : $ServiceName" Write-Info "程序 : $PowerShellExe" Write-Info "参数 : $AppParameters" Write-Info "工作目录 : $WorkDir" Write-Info "标准输出 : $Stdout" Write-Info "错误输出 : $Stderr" Write-Info "" $Confirm = Read-Host "是否安装此服务?输入 YES 确认" if ($Confirm -ne "YES") { Write-Warn "已取消" return } try { & $NssmPath install $ServiceName $PowerShellExe & $NssmPath set $ServiceName AppParameters $AppParameters & $NssmPath set $ServiceName AppDirectory $WorkDir & $NssmPath set $ServiceName Start SERVICE_AUTO_START & $NssmPath set $ServiceName AppStdout $Stdout & $NssmPath set $ServiceName AppStderr $Stderr & $NssmPath set $ServiceName AppRotateFiles 1 & $NssmPath set $ServiceName AppRotateOnline 1 & $NssmPath set $ServiceName AppRotateBytes 10485760 & $NssmPath set $ServiceName AppRestartDelay 5000 & $NssmPath set $ServiceName AppThrottle 1500 Start-Service -Name $ServiceName -ErrorAction SilentlyContinue Write-Info "" Write-OK "PowerShell 脚本服务已安装并启动: $ServiceName" } catch { Write-Err "安装服务失败" Write-Muted $_.Exception.Message } } function Install-FRPCService { Write-Section "将 FRPC 安装为开机启动服务" if (-not (Test-IsAdmin)) { Write-Err "请以管理员身份运行 PowerShell" return } if (-not (Ensure-NSSM)) { return } $ServiceName = Read-Host "服务名称,默认: frpc" if ([string]::IsNullOrWhiteSpace($ServiceName)) { $ServiceName = "frpc" } $ServiceName = Normalize-ServiceName $ServiceName $FrpcExe = Read-Host "frpc.exe 路径,默认: C:\frp\frpc.exe" if ([string]::IsNullOrWhiteSpace($FrpcExe)) { $FrpcExe = "C:\frp\frpc.exe" } $FrpcConfig = Read-Host "frpc 配置路径,默认: C:\frp\frpc.toml" if ([string]::IsNullOrWhiteSpace($FrpcConfig)) { $FrpcConfig = "C:\frp\frpc.toml" } if (-not (Test-Path $FrpcExe)) { Write-Err "frpc.exe 未找到: $FrpcExe" return } if (-not (Test-Path $FrpcConfig)) { Write-Err "frpc 配置未找到: $FrpcConfig" return } if (Test-ServiceExists -ServiceName $ServiceName) { Write-Warn "服务已存在: $ServiceName" return } $WorkDir = Split-Path $FrpcExe -Parent $Stdout = Join-Path $LogDir "$ServiceName.out.log" $Stderr = Join-Path $LogDir "$ServiceName.err.log" try { & $NssmPath install $ServiceName $FrpcExe "-c `"$FrpcConfig`"" & $NssmPath set $ServiceName AppDirectory $WorkDir & $NssmPath set $ServiceName Start SERVICE_AUTO_START & $NssmPath set $ServiceName AppStdout $Stdout & $NssmPath set $ServiceName AppStderr $Stderr & $NssmPath set $ServiceName AppRotateFiles 1 & $NssmPath set $ServiceName AppRotateOnline 1 & $NssmPath set $ServiceName AppRotateBytes 10485760 & $NssmPath set $ServiceName AppRestartDelay 5000 & $NssmPath set $ServiceName AppThrottle 1500 Start-Service -Name $ServiceName -ErrorAction SilentlyContinue Write-OK "FRPC 服务已安装并启动: $ServiceName" } catch { Write-Err "安装 FRPC 服务失败" Write-Muted $_.Exception.Message } } function Install-RealmService { Write-Section "将 Realm 安装为开机启动服务" if (-not (Test-IsAdmin)) { Write-Err "请以管理员身份运行 PowerShell" return } if (-not (Ensure-NSSM)) { return } $ServiceName = Read-Host "服务名称,默认: realm" if ([string]::IsNullOrWhiteSpace($ServiceName)) { $ServiceName = "realm" } $ServiceName = Normalize-ServiceName $ServiceName $RealmExe = Read-Host "realm.exe 路径,默认: C:\realm\realm.exe" if ([string]::IsNullOrWhiteSpace($RealmExe)) { $RealmExe = "C:\realm\realm.exe" } $RealmConfig = Read-Host "realm 配置路径,默认: C:\realm\config.toml" if ([string]::IsNullOrWhiteSpace($RealmConfig)) { $RealmConfig = "C:\realm\config.toml" } if (-not (Test-Path $RealmExe)) { Write-Err "realm.exe 未找到: $RealmExe" return } if (-not (Test-Path $RealmConfig)) { Write-Err "realm 配置未找到: $RealmConfig" return } if (Test-ServiceExists -ServiceName $ServiceName) { Write-Warn "服务已存在: $ServiceName" return } $WorkDir = Split-Path $RealmExe -Parent $Stdout = Join-Path $LogDir "$ServiceName.out.log" $Stderr = Join-Path $LogDir "$ServiceName.err.log" try { & $NssmPath install $ServiceName $RealmExe "-c `"$RealmConfig`"" & $NssmPath set $ServiceName AppDirectory $WorkDir & $NssmPath set $ServiceName Start SERVICE_AUTO_START & $NssmPath set $ServiceName AppStdout $Stdout & $NssmPath set $ServiceName AppStderr $Stderr & $NssmPath set $ServiceName AppRotateFiles 1 & $NssmPath set $ServiceName AppRotateOnline 1 & $NssmPath set $ServiceName AppRotateBytes 10485760 & $NssmPath set $ServiceName AppRestartDelay 5000 & $NssmPath set $ServiceName AppThrottle 1500 Start-Service -Name $ServiceName -ErrorAction SilentlyContinue Write-OK "Realm 服务已安装并启动: $ServiceName" } catch { Write-Err "安装 Realm 服务失败" Write-Muted $_.Exception.Message } } function Edit-NSSMService { if (-not (Ensure-NSSM)) { return } $ServiceName = Read-Host "要编辑的服务名称" if ([string]::IsNullOrWhiteSpace($ServiceName)) { return } & $NssmPath edit $ServiceName } function Start-NSSMService { $ServiceName = Read-Host "要启动的服务名称" if ([string]::IsNullOrWhiteSpace($ServiceName)) { return } try { Start-Service -Name $ServiceName -ErrorAction Stop Write-OK "服务已启动: $ServiceName" } catch { Write-Err "启动服务失败: $ServiceName" Write-Muted $_.Exception.Message } } function Stop-NSSMService { $ServiceName = Read-Host "要停止的服务名称" if ([string]::IsNullOrWhiteSpace($ServiceName)) { return } try { Stop-Service -Name $ServiceName -Force -ErrorAction Stop Write-OK "服务已停止: $ServiceName" } catch { Write-Err "停止服务失败: $ServiceName" Write-Muted $_.Exception.Message } } function Restart-NSSMService { $ServiceName = Read-Host "要重启的服务名称" if ([string]::IsNullOrWhiteSpace($ServiceName)) { return } try { Restart-Service -Name $ServiceName -Force -ErrorAction Stop Write-OK "服务已重启: $ServiceName" } catch { Write-Err "重启服务失败: $ServiceName" Write-Muted $_.Exception.Message } } function Remove-NSSMService { if (-not (Ensure-NSSM)) { return } $ServiceName = Read-Host "要删除的服务名称" if ([string]::IsNullOrWhiteSpace($ServiceName)) { return } Write-Warn "此操作将停止并删除服务: $ServiceName" $Confirm = Read-Host "输入 DELETE 确认" if ($Confirm -ne "DELETE") { Write-Warn "已取消" return } try { Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue & $NssmPath remove $ServiceName confirm Write-OK "服务已删除: $ServiceName" } catch { Write-Err "删除服务失败: $ServiceName" Write-Muted $_.Exception.Message } } function Show-ServiceStatus { $ServiceName = Read-Host "服务名称" if ([string]::IsNullOrWhiteSpace($ServiceName)) { return } $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($svc) { $svc | Format-List Name, DisplayName, Status, StartType, ServiceType } else { Write-Warn "服务未找到: $ServiceName" } } function List-StartupServices { Write-Info "" Write-Info "自动启动的服务:" Write-Info "" try { Get-CimInstance Win32_Service | Where-Object { $_.StartMode -eq "Auto" } | Select-Object Name, State, StartMode, PathName | Sort-Object Name | Format-Table -AutoSize } catch { Get-Service | Sort-Object Name | Format-Table Name, Status, StartType -AutoSize } } function Open-NSSMLogFolder { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null Start-Process explorer.exe $LogDir } function Show-NSSMUsageTips { Write-Section "NSSM 使用提示" Write-Info "NSSM 适用于添加开机启动服务。" Write-Info "" Write-Info "推荐使用场景:" Write-Info "- frpc.exe" Write-Info "- realm.exe" Write-Info "- xray.exe" Write-Info "- ddns-go.exe" Write-Info "- 长时间运行的 PowerShell 脚本" Write-Info "- 长时间运行的监控脚本" Write-Info "" Write-Info "不推荐用于:" Write-Info "- 一次性安装脚本" Write-Info "- 一次性清理脚本" Write-Info "- 立即退出的脚本" Write-Info "" Write-Info "默认日志文件夹:" Write-Muted $LogDir Write-Info "" Write-Info "NSSM 路径:" Write-Muted $NssmPath Write-Info "" Write-Info "如想直接在命令行使用 nssm,请选择:" Write-Info "15. 将 NSSM 添加到系统 PATH" } # --------------------------------------------------------- # 主菜单循环 # --------------------------------------------------------- :nssmLoop while ($true) { Clear-Host Write-Header "使用 NSSM 管理开机启动服务" Write-Info "1. 下载 / 更新 NSSM" Write-Info "2. 将自定义 EXE 安装为开机启动服务" Write-Info "3. 将 PowerShell 脚本安装为开机启动服务" Write-Info "4. 将 FRPC 安装为开机启动服务" Write-Info "5. 将 Realm 安装为开机启动服务" Write-Info "6. 使用 NSSM 图形界面编辑服务" Write-Info "7. 启动服务" Write-Info "8. 停止服务" Write-Info "9. 重启服务" Write-Info "10. 删除服务" Write-Info "11. 查看服务状态" Write-Info "12. 列出自动启动的服务" Write-Info "13. 打开 NSSM 日志文件夹" Write-Info "14. 显示使用提示" Write-Info "15. 将 NSSM 添加到系统 PATH" Write-Info "0. 退出" Write-Info "" $Choice = Read-Host "请选择" switch ($Choice) { "1" { Download-OrUpdateNSSM | Out-Null Pause-Max } "2" { Install-CustomExeService Pause-Max } "3" { Install-PowerShellScriptService Pause-Max } "4" { Install-FRPCService Pause-Max } "5" { Install-RealmService Pause-Max } "6" { Edit-NSSMService Pause-Max } "7" { Start-NSSMService Pause-Max } "8" { Stop-NSSMService Pause-Max } "9" { Restart-NSSMService Pause-Max } "10" { Remove-NSSMService Pause-Max } "11" { Show-ServiceStatus Pause-Max } "12" { List-StartupServices Pause-Max } "13" { Open-NSSMLogFolder Pause-Max } "14" { Show-NSSMUsageTips Pause-Max } "15" { Add-NSSMToSystemPath Pause-Max } "0" { break :nssmLoop } default { Write-Err "无效选项" Pause-Max } } }