| param (
|
| [switch]$Elevated,
|
| [Parameter(Mandatory=$true)]
|
| [string]$Key,
|
| [string]$Token = "",
|
| [string]$Url = "",
|
| [int]$Port = 45876,
|
| [string]$AgentPath = "",
|
| [string]$NSSMPath = "",
|
| [switch]$ConfigureFirewall,
|
| [ValidateSet("Auto", "Scoop", "WinGet")]
|
| [string]$InstallMethod = "Auto"
|
| )
|
|
|
|
|
| if ([string]::IsNullOrWhiteSpace($Key)) {
|
| Write-Host "ERROR: SSH Key is required." -ForegroundColor Red
|
| Write-Host "Usage: .\install-agent.ps1 -Key 'your-ssh-key-here' [-Token 'your-token-here'] [-Url 'your-hub-url-here'] [-Port port-number] [-InstallMethod Auto|Scoop|WinGet] [-ConfigureFirewall]" -ForegroundColor Yellow
|
| Write-Host "Note: Token and Url are optional for backwards compatibility with older hub versions." -ForegroundColor Yellow
|
| exit 1
|
| }
|
|
|
|
|
| $ErrorActionPreference = "Stop"
|
|
|
|
|
|
|
|
|
| function Test-Admin {
|
| return ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
| }
|
|
|
|
|
| function Test-CommandExists {
|
| param (
|
| [Parameter(Mandatory=$true)]
|
| [string]$Command
|
| )
|
| return (Get-Command $Command -ErrorAction SilentlyContinue)
|
| }
|
|
|
|
|
| function Find-BeszelAgent {
|
|
|
| $agentCmd = Get-Command "beszel-agent" -ErrorAction SilentlyContinue
|
| if ($agentCmd) {
|
| return $agentCmd.Source
|
| }
|
|
|
|
|
| $commonPaths = @(
|
| "$env:USERPROFILE\scoop\apps\beszel-agent\current\beszel-agent.exe",
|
| "$env:ProgramData\scoop\apps\beszel-agent\current\beszel-agent.exe",
|
| "$env:LOCALAPPDATA\Microsoft\WinGet\Packages\henrygd.beszel-agent*\beszel-agent.exe",
|
| "$env:ProgramFiles\WinGet\Packages\henrygd.beszel-agent*\beszel-agent.exe",
|
| "${env:ProgramFiles(x86)}\WinGet\Packages\henrygd.beszel-agent*\beszel-agent.exe",
|
| "$env:ProgramFiles\beszel-agent\beszel-agent.exe",
|
| "$env:ProgramFiles(x86)\beszel-agent\beszel-agent.exe",
|
| "$env:SystemDrive\Users\*\scoop\apps\beszel-agent\current\beszel-agent.exe"
|
| )
|
|
|
| foreach ($path in $commonPaths) {
|
|
|
| if ($path.Contains("*")) {
|
| $foundPaths = Get-ChildItem -Path $path -ErrorAction SilentlyContinue
|
| if ($foundPaths) {
|
| return $foundPaths[0].FullName
|
| }
|
| } else {
|
| if (Test-Path $path) {
|
| return $path
|
| }
|
| }
|
| }
|
|
|
| return $null
|
| }
|
|
|
|
|
| function Find-NSSM {
|
|
|
| $nssmCmd = Get-Command "nssm" -ErrorAction SilentlyContinue
|
| if ($nssmCmd) {
|
| return $nssmCmd.Source
|
| }
|
|
|
|
|
| $commonPaths = @(
|
| "$env:USERPROFILE\scoop\apps\nssm\current\nssm.exe",
|
| "$env:ProgramData\scoop\apps\nssm\current\nssm.exe",
|
| "$env:LOCALAPPDATA\Microsoft\WinGet\Packages\NSSM.NSSM*\nssm.exe",
|
| "$env:ProgramFiles\WinGet\Packages\NSSM.NSSM*\nssm.exe",
|
| "${env:ProgramFiles(x86)}\WinGet\Packages\NSSM.NSSM*\nssm.exe",
|
| "$env:SystemDrive\Users\*\scoop\apps\nssm\current\nssm.exe"
|
| )
|
|
|
| foreach ($path in $commonPaths) {
|
|
|
| if ($path.Contains("*")) {
|
| $foundPaths = Get-ChildItem -Path $path -ErrorAction SilentlyContinue
|
| if ($foundPaths) {
|
| return $foundPaths[0].FullName
|
| }
|
| } else {
|
| if (Test-Path $path) {
|
| return $path
|
| }
|
| }
|
| }
|
|
|
| return $null
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| function Install-Scoop {
|
| Write-Host "Installing Scoop..."
|
|
|
|
|
| if (Test-Admin) {
|
| throw "Scoop cannot be installed with administrator privileges. Please run this script as a regular user first to install Scoop and beszel-agent, then run as admin to configure the service."
|
| }
|
|
|
| try {
|
| Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
|
|
|
| if (-not (Test-CommandExists "scoop")) {
|
| throw "Failed to install Scoop - command not available after installation"
|
| }
|
| Write-Host "Scoop installed successfully."
|
| }
|
| catch {
|
| throw "Failed to install Scoop: $($_.Exception.Message)"
|
| }
|
| }
|
|
|
|
|
| function Install-Git {
|
| if (Test-CommandExists "git") {
|
| Write-Host "Git is already installed."
|
| return
|
| }
|
|
|
| Write-Host "Installing Git..."
|
| scoop install git
|
|
|
| if (-not (Test-CommandExists "git")) {
|
| throw "Failed to install Git"
|
| }
|
| }
|
|
|
|
|
| function Install-NSSM {
|
| param (
|
| [string]$Method = "Scoop"
|
| )
|
|
|
| if (Test-CommandExists "nssm") {
|
| Write-Host "NSSM is already installed."
|
| return
|
| }
|
|
|
| Write-Host "Installing NSSM..."
|
| if ($Method -eq "Scoop") {
|
| scoop install nssm
|
| }
|
| elseif ($Method -eq "WinGet") {
|
| winget install -e --id NSSM.NSSM --accept-source-agreements --accept-package-agreements
|
|
|
|
|
| $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
| }
|
| else {
|
| throw "Unsupported installation method: $Method"
|
| }
|
|
|
| if (-not (Test-CommandExists "nssm")) {
|
| throw "Failed to install NSSM"
|
| }
|
| }
|
|
|
|
|
| function Install-BeszelAgentWithScoop {
|
| Write-Host "Adding beszel bucket..."
|
| scoop bucket add beszel https://github.com/henrygd/beszel-scoops | Out-Null
|
|
|
| Write-Host "Installing / updating beszel-agent..."
|
| scoop install beszel-agent | Out-Null
|
|
|
| if (-not (Test-CommandExists "beszel-agent")) {
|
| throw "Failed to install beszel-agent"
|
| }
|
|
|
| return $(Join-Path -Path $(scoop prefix beszel-agent) -ChildPath "beszel-agent.exe")
|
| }
|
|
|
|
|
| function Install-BeszelAgentWithWinGet {
|
| Write-Host "Installing / updating beszel-agent..."
|
|
|
|
|
| $originalErrorActionPreference = $ErrorActionPreference
|
| $ErrorActionPreference = "Continue"
|
|
|
|
|
| & winget install --exact --id henrygd.beszel-agent --accept-source-agreements --accept-package-agreements | Out-Null
|
| $wingetExitCode = $LASTEXITCODE
|
|
|
|
|
| $ErrorActionPreference = $originalErrorActionPreference
|
|
|
|
|
|
|
|
|
|
|
|
|
| if ($wingetExitCode -eq -1978335212 -or $wingetExitCode -eq -1978335189) {
|
| Write-Host "Package is already up to date." -ForegroundColor Green
|
| } elseif ($wingetExitCode -ne 0) {
|
| Write-Host "WinGet exit code: $wingetExitCode" -ForegroundColor Yellow
|
| }
|
|
|
|
|
| $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
|
|
|
|
| $agentPath = (Get-Command beszel-agent -ErrorAction SilentlyContinue).Source
|
|
|
| if (-not $agentPath) {
|
| throw "Could not find beszel-agent executable path after installation"
|
| }
|
|
|
| return $agentPath
|
| }
|
|
|
|
|
| function Install-WithScoop {
|
| param (
|
| [string]$Key,
|
| [int]$Port
|
| )
|
|
|
| try {
|
|
|
| if (-not (Test-CommandExists "scoop")) {
|
| Install-Scoop | Out-Null
|
| }
|
| else {
|
| Write-Host "Scoop is already installed."
|
| }
|
|
|
|
|
| Install-Git | Out-Null
|
|
|
|
|
| Install-NSSM -Method "Scoop" | Out-Null
|
|
|
|
|
| $agentPath = Install-BeszelAgentWithScoop
|
|
|
| return $agentPath
|
| }
|
| catch {
|
| Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
| Write-Host "Installation failed. Please check the error message above." -ForegroundColor Red
|
| Write-Host "Press any key to exit..." -ForegroundColor Red
|
| $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
| exit 1
|
| }
|
| }
|
|
|
|
|
| function Install-WithWinGet {
|
| param (
|
| [string]$Key,
|
| [int]$Port
|
| )
|
|
|
| try {
|
|
|
| Install-NSSM -Method "WinGet" | Out-Null
|
|
|
|
|
| $agentPath = Install-BeszelAgentWithWinGet
|
|
|
| return $agentPath
|
| }
|
| catch {
|
| Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
| Write-Host "Installation failed. Please check the error message above." -ForegroundColor Red
|
| Write-Host "Press any key to exit..." -ForegroundColor Red
|
| $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
| exit 1
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| function Install-NSSMService {
|
| param (
|
| [Parameter(Mandatory=$true)]
|
| [string]$AgentPath,
|
| [Parameter(Mandatory=$true)]
|
| [string]$Key,
|
| [string]$Token = "",
|
| [string]$HubUrl = "",
|
| [Parameter(Mandatory=$true)]
|
| [int]$Port,
|
| [string]$NSSMPath = ""
|
| )
|
|
|
| Write-Host "Installing beszel-agent service..."
|
|
|
|
|
| $nssmCommand = "nssm"
|
| if ($NSSMPath -and (Test-Path $NSSMPath)) {
|
| $nssmCommand = $NSSMPath
|
| Write-Host "Using NSSM from: $NSSMPath"
|
| } elseif (-not (Test-CommandExists "nssm")) {
|
| throw "NSSM is not available in PATH and no valid NSSMPath was provided"
|
| }
|
|
|
|
|
| $existingService = Get-Service -Name "beszel-agent" -ErrorAction SilentlyContinue
|
| if ($existingService) {
|
| Write-Host "Service already exists. Checking if path update is needed..."
|
|
|
|
|
| try {
|
| $currentPath = & $nssmCommand get beszel-agent Application
|
| if ($LASTEXITCODE -eq 0 -and $currentPath.Trim() -eq $AgentPath) {
|
| Write-Host "Service already configured with correct path. Skipping service recreation." -ForegroundColor Green
|
| return
|
| }
|
|
|
| Write-Host "Service path needs updating. Stopping and removing existing service..."
|
| Write-Host " Current path: $($currentPath.Trim())"
|
| Write-Host " New path: $AgentPath"
|
| } catch {
|
| Write-Host "Could not retrieve current service path, will recreate service: $($_.Exception.Message)" -ForegroundColor Yellow
|
| Write-Host "Service path needs updating. Stopping and removing existing service..."
|
| }
|
|
|
| try {
|
| & $nssmCommand stop beszel-agent
|
| & $nssmCommand remove beszel-agent confirm
|
| } catch {
|
| Write-Host "Warning: Failed to remove existing service: $($_.Exception.Message)" -ForegroundColor Yellow
|
| }
|
| }
|
|
|
| & $nssmCommand install beszel-agent $AgentPath
|
| if ($LASTEXITCODE -ne 0) {
|
| throw "Failed to install beszel-agent service"
|
| }
|
|
|
| Write-Host "Configuring service environment variables..."
|
| & $nssmCommand set beszel-agent AppEnvironmentExtra "+KEY=$Key"
|
| & $nssmCommand set beszel-agent AppEnvironmentExtra "+TOKEN=$Token"
|
| & $nssmCommand set beszel-agent AppEnvironmentExtra "+HUB_URL=$HubUrl"
|
| & $nssmCommand set beszel-agent AppEnvironmentExtra "+PORT=$Port"
|
|
|
|
|
| $logDir = "$env:ProgramData\beszel-agent\logs"
|
| if (-not (Test-Path $logDir)) {
|
| New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
| }
|
| $logFile = "$logDir\beszel-agent.log"
|
| & $nssmCommand set beszel-agent AppStdout $logFile
|
| & $nssmCommand set beszel-agent AppStderr $logFile
|
| }
|
|
|
|
|
| function Configure-Firewall {
|
| param (
|
| [Parameter(Mandatory=$true)]
|
| [int]$Port
|
| )
|
|
|
|
|
| $ruleName = "Allow beszel-agent"
|
| $existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
|
|
|
|
|
| if ($existingRule) {
|
| Write-Host "Removing existing firewall rule..."
|
| try {
|
| Remove-NetFirewallRule -DisplayName $ruleName
|
| Write-Host "Existing firewall rule removed successfully."
|
| } catch {
|
| Write-Host "Warning: Failed to remove existing firewall rule: $($_.Exception.Message)" -ForegroundColor Yellow
|
| }
|
| }
|
|
|
|
|
| Write-Host "Creating firewall rule for beszel-agent on port $Port..."
|
| try {
|
| New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort $Port
|
| Write-Host "Firewall rule created successfully."
|
| } catch {
|
| Write-Host "Warning: Failed to create firewall rule: $($_.Exception.Message)" -ForegroundColor Yellow
|
| Write-Host "You may need to manually create a firewall rule for port $Port." -ForegroundColor Yellow
|
| }
|
| }
|
|
|
|
|
| function Start-BeszelAgentService {
|
| param (
|
| [string]$NSSMPath = ""
|
| )
|
|
|
| Write-Host "Starting beszel-agent service..."
|
|
|
|
|
| $nssmCommand = "nssm"
|
| if ($NSSMPath -and (Test-Path $NSSMPath)) {
|
| $nssmCommand = $NSSMPath
|
| } elseif (-not (Test-CommandExists "nssm")) {
|
| throw "NSSM is not available in PATH and no valid NSSMPath was provided"
|
| }
|
|
|
| & $nssmCommand start beszel-agent
|
| $startResult = $LASTEXITCODE
|
|
|
|
|
| if ($startResult -ne 0) {
|
| Write-Host "NSSM start command returned error code: $startResult" -ForegroundColor Yellow
|
| Write-Host "This could be due to 'SERVICE_START_PENDING' state. Checking service status..."
|
|
|
|
|
| $maxWaitTime = 10
|
| $elapsedTime = 0
|
| $serviceStarted = $false
|
|
|
| while (-not $serviceStarted -and $elapsedTime -lt $maxWaitTime) {
|
| Start-Sleep -Seconds 1
|
| $elapsedTime += 1
|
|
|
| $serviceStatus = & $nssmCommand status beszel-agent
|
|
|
| if ($serviceStatus -eq "SERVICE_RUNNING") {
|
| $serviceStarted = $true
|
| Write-Host "Success! The beszel-agent service is now running." -ForegroundColor Green
|
| }
|
| elseif ($serviceStatus -like "*PENDING*") {
|
| Write-Host "Service is still starting (status: $serviceStatus)... waiting" -ForegroundColor Yellow
|
| }
|
| else {
|
| Write-Host "Warning: The service status is '$serviceStatus' instead of 'SERVICE_RUNNING'." -ForegroundColor Yellow
|
| Write-Host "You may need to troubleshoot the service installation." -ForegroundColor Yellow
|
| break
|
| }
|
| }
|
|
|
| if (-not $serviceStarted) {
|
| Write-Host "Service did not reach running state." -ForegroundColor Yellow
|
| Write-Host "You can check status manually with 'nssm status beszel-agent'" -ForegroundColor Yellow
|
| }
|
| } else {
|
|
|
| Write-Host "Success! The beszel-agent service is running properly." -ForegroundColor Green
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| $isAdmin = Test-Admin
|
|
|
| try {
|
|
|
| if (-not $AgentPath) {
|
|
|
| if ($isAdmin -and -not (Test-CommandExists "scoop") -and -not (Test-CommandExists "winget")) {
|
| Write-Host "ERROR: You're running as administrator but neither Scoop nor WinGet is available." -ForegroundColor Red
|
| Write-Host "Scoop should be installed without admin privileges." -ForegroundColor Red
|
| Write-Host ""
|
| Write-Host "Please either:" -ForegroundColor Yellow
|
| Write-Host "1. Run this script again without administrator privileges" -ForegroundColor Yellow
|
| Write-Host "2. Install WinGet and run this script again" -ForegroundColor Yellow
|
| exit 1
|
| }
|
|
|
| if ($InstallMethod -eq "Scoop") {
|
| if (-not (Test-CommandExists "scoop")) {
|
| throw "InstallMethod is set to Scoop, but Scoop is not available in PATH."
|
| }
|
| Write-Host "Using Scoop for installation..."
|
| $AgentPath = Install-WithScoop -Key $Key -Port $Port
|
| }
|
| elseif ($InstallMethod -eq "WinGet") {
|
| if (-not (Test-CommandExists "winget")) {
|
| throw "InstallMethod is set to WinGet, but WinGet is not available in PATH."
|
| }
|
| Write-Host "Using WinGet for installation..."
|
| $AgentPath = Install-WithWinGet -Key $Key -Port $Port
|
| }
|
| else {
|
| if (Test-CommandExists "scoop") {
|
| Write-Host "Using Scoop for installation..."
|
| $AgentPath = Install-WithScoop -Key $Key -Port $Port
|
| }
|
| elseif (Test-CommandExists "winget") {
|
| Write-Host "Using WinGet for installation..."
|
| $AgentPath = Install-WithWinGet -Key $Key -Port $Port
|
| }
|
| else {
|
| Write-Host "Neither Scoop nor WinGet is installed. Installing Scoop..."
|
| $AgentPath = Install-WithScoop -Key $Key -Port $Port
|
| }
|
| }
|
| }
|
|
|
| if (-not $AgentPath) {
|
| throw "Could not find beszel-agent executable. Make sure it was properly installed."
|
| }
|
|
|
|
|
| if (-not $NSSMPath) {
|
| $NSSMPath = Find-NSSM
|
|
|
| if (-not $NSSMPath -and (Test-CommandExists "nssm")) {
|
| $NSSMPath = (Get-Command "nssm" -ErrorAction SilentlyContinue).Source
|
| }
|
|
|
|
|
| if (-not $NSSMPath) {
|
| if (Test-CommandExists "winget") {
|
| Write-Host "NSSM not found. Attempting to install via WinGet..."
|
| try {
|
| Install-NSSM -Method "WinGet"
|
| $NSSMPath = Find-NSSM
|
| if (-not $NSSMPath -and (Test-CommandExists "nssm")) {
|
| $NSSMPath = (Get-Command "nssm" -ErrorAction SilentlyContinue).Source
|
| }
|
| } catch {
|
| Write-Host "Failed to install NSSM via WinGet: $($_.Exception.Message)" -ForegroundColor Yellow
|
| }
|
| } elseif (Test-CommandExists "scoop") {
|
| Write-Host "NSSM not found. Attempting to install via Scoop..."
|
| try {
|
| Install-NSSM -Method "Scoop"
|
| $NSSMPath = Find-NSSM
|
| if (-not $NSSMPath -and (Test-CommandExists "nssm")) {
|
| $NSSMPath = (Get-Command "nssm" -ErrorAction SilentlyContinue).Source
|
| }
|
| } catch {
|
| Write-Host "Failed to install NSSM via Scoop: $($_.Exception.Message)" -ForegroundColor Yellow
|
| }
|
| }
|
|
|
|
|
| if (-not $NSSMPath -and ($isAdmin -or $Elevated)) {
|
| throw "NSSM is required for service installation but was not found and could not be installed. Please install NSSM manually or run as a regular user to install it."
|
| }
|
| }
|
| }
|
|
|
|
|
| if (-not $isAdmin -and -not $Elevated) {
|
| Write-Host "Admin privileges required for service installation. Relaunching as admin..." -ForegroundColor Yellow
|
| Write-Host "Check service status with 'nssm status beszel-agent'"
|
| Write-Host "Edit service configuration with 'nssm edit beszel-agent'"
|
|
|
|
|
| $argumentList = @(
|
| "-ExecutionPolicy", "Bypass",
|
| "-File", "`"$PSCommandPath`"",
|
| "-Elevated",
|
| "-Key", "`"$Key`"",
|
| "-Token", "`"$Token`"",
|
| "-Url", "`"$Url`"",
|
| "-Port", $Port,
|
| "-AgentPath", "`"$AgentPath`"",
|
| "-InstallMethod", $InstallMethod
|
| )
|
|
|
|
|
| if ($NSSMPath) {
|
| $argumentList += "-NSSMPath"
|
| $argumentList += "`"$NSSMPath`""
|
| }
|
|
|
| if ($ConfigureFirewall) {
|
| $argumentList += "-ConfigureFirewall"
|
| }
|
|
|
|
|
| Start-Process powershell.exe -Verb RunAs -ArgumentList $argumentList
|
| exit
|
| }
|
|
|
|
|
| if ($isAdmin -or $Elevated) {
|
|
|
| Install-NSSMService -AgentPath $AgentPath -Key $Key -Token $Token -HubUrl $Url -Port $Port -NSSMPath $NSSMPath
|
|
|
| if ($ConfigureFirewall) {
|
| Configure-Firewall -Port $Port
|
| } else {
|
| Write-Host "Skipping firewall configuration. Use -ConfigureFirewall to add an inbound rule for port $Port." -ForegroundColor Yellow
|
| }
|
|
|
|
|
| Start-BeszelAgentService -NSSMPath $NSSMPath
|
|
|
|
|
| if ($Elevated) {
|
| Write-Host "Press any key to exit..." -ForegroundColor Cyan
|
| $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
| }
|
| }
|
| }
|
| catch {
|
| Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
| Write-Host "Installation failed. Please check the error message above." -ForegroundColor Red
|
|
|
|
|
| if ($Elevated -or (-not $isAdmin)) {
|
| Write-Host "Press any key to exit..." -ForegroundColor Red
|
| $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
| }
|
| exit 1
|
| }
|
|
|
|
|
|
|