# Windows PowerShell installation script for Komari Agent # Logging functions with colors function Log-Info { param([string]$Message) Write-Host "$Message" -ForegroundColor Cyan } function Log-Success { param([string]$Message) Write-Host "$Message" -ForegroundColor Green } function Log-Warning { param([string]$Message) Write-Host "[WARNING] $Message" -ForegroundColor Yellow } function Log-Error { param([string]$Message) Write-Host "[ERROR] $Message" -ForegroundColor Red } function Log-Step { param([string]$Message) Write-Host "$Message" -ForegroundColor Magenta } function Log-Config { param([string]$Message) Write-Host "- $Message" -ForegroundColor White } # Default parameters $InstallDir = Join-Path $Env:ProgramFiles "Komari" $ServiceName = "komari-agent" $GitHubProxy = "" $KomariArgs = @() $InstallVersion = "" # Parse script arguments for ($i = 0; $i -lt $args.Count; $i++) { switch ($args[$i]) { "--install-dir" { $InstallDir = $args[$i + 1]; $i++; continue } "--install-service-name" { $ServiceName = $args[$i + 1]; $i++; continue } "--install-ghproxy" { $GitHubProxy = $args[$i + 1]; $i++; continue } "--install-version" { $InstallVersion = $args[$i + 1]; $i++; continue } Default { $KomariArgs += $args[$i] } } } # Quote a value the way the Windows C runtime command-line parser expects, # so arguments survive command-line re-quoting boundaries (Start-Process # -ArgumentList, powershell -File, cmd /c, ...) even when they contain # spaces such as C:\Users\John Doe\... (komari-monitor/komari#655). function ConvertTo-CommandLineArg { param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Value) if ($Value -notmatch '[\s"]') { return $Value } $escaped = $Value -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1' return '"' + $escaped + '"' } # Ensure running as Administrator. Instead of failing, re-launch this script # elevated and re-quote its own arguments: wrappers that elevate externally # (Start-Process and friends) regularly split arguments of paths containing # spaces, which breaks installs for user names with spaces (komari#655). if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { if ([string]::IsNullOrEmpty($PSCommandPath)) { Log-Error "Cannot elevate a script that was not run from a file. Save it as install.ps1 and run it again as Administrator." exit 1 } Log-Warning "Administrator privileges are required. Requesting elevation, please accept the UAC prompt..." $hostExe = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (ConvertTo-CommandLineArg $PSCommandPath)) foreach ($a in $args) { $relaunchArgs += ConvertTo-CommandLineArg ([string]$a) } try { $elevated = Start-Process -FilePath $hostExe -Verb RunAs -Wait -PassThru -ArgumentList ($relaunchArgs -join ' ') exit $elevated.ExitCode } catch { Log-Error "Elevation failed or was cancelled: $_" Log-Error "Please run this script as Administrator." exit 1 } } # Prepare GitHub proxy display if ($GitHubProxy -ne '') { $ProxyDisplay = $GitHubProxy } else { $ProxyDisplay = '(direct)' } # Detect architecture early for constructing binary name switch ($env:PROCESSOR_ARCHITECTURE) { 'AMD64' { $arch = 'amd64' } 'ARM64' { $arch = 'arm64' } 'x86' { $arch = '386' } Default { Log-Error "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE"; exit 1 } } # Ensure installation directory exists for nssm and agent Log-Step "Ensuring installation directory exists: $InstallDir" New-Item -ItemType Directory -Path $InstallDir -Force -ErrorAction SilentlyContinue | Out-Null # Ensure $InstallDir exists # Check for nssm and download if not present $nssmExeToUse = Join-Path $InstallDir "nssm.exe" # First, check if nssm is in PATH and is functional $nssmCmd = Get-Command nssm -ErrorAction SilentlyContinue if ($nssmCmd) { Log-Info "nssm found in PATH at $($nssmCmd.Source)." try { $nssmVersionOutput = nssm version 2>&1 Log-Info "Detected nssm version: $nssmVersionOutput" } catch { Log-Warning "nssm found in PATH failed to execute 'nssm version'. Will attempt to use/download local copy. Error: $_" $nssmCmd = $null # Force re-evaluation for local copy or download } } # If nssm not found in PATH or the one in PATH failed, check local $InstallDir if (-not $nssmCmd) { if (Test-Path $nssmExeToUse) { Log-Info "nssm found at $nssmExeToUse. Attempting to use it by adding $InstallDir to PATH." $env:Path = "$($InstallDir);$($env:Path)" $nssmCmd = Get-Command nssm -ErrorAction SilentlyContinue if ($nssmCmd) { try { $nssmVersionOutput = nssm version 2>&1 } catch { Log-Warning "nssm from $InstallDir failed to execute 'nssm version'. Error: $_" $nssmCmd = $null # Mark as unusable } } else { Log-Warning "Failed to make nssm from $nssmExeToUse available via PATH. Will attempt download." } } } # If still no usable nssm command, proceed to download if (-not $nssmCmd) { Log-Info "nssm not found or not usable. Attempting to download to $InstallDir..." $NssmVersion = "2.24" $NssmZipUrl = "https://nssm.cc/release/nssm-$NssmVersion.zip" $TempNssmZipPath = Join-Path $env:TEMP "nssm-$NssmVersion.zip" $TempExtractDir = Join-Path $env:TEMP "nssm_extract_temp" try { Log-Info "Downloading nssm from $NssmZipUrl..." Invoke-WebRequest -Uri $NssmZipUrl -OutFile $TempNssmZipPath -UseBasicParsing if (Test-Path $TempExtractDir) { Remove-Item -Recurse -Force $TempExtractDir } New-Item -ItemType Directory -Path $TempExtractDir -Force | Out-Null Expand-Archive -Path $TempNssmZipPath -DestinationPath $TempExtractDir -Force $NssmSourceDirInsideZip = "nssm-$NssmVersion" # Used for Get-ChildItem search path # The path part within the extracted nssm folder, e.g., "nssm-2.24\win32" # 'win32' nssm is used for both 'amd64' and 'arm64' PowerShell architectures. $NssmArchSubDir = Join-Path "nssm-$NssmVersion" "win32" $NssmSourceExePath = Join-Path (Join-Path $TempExtractDir $NssmArchSubDir) "nssm.exe" if (-not (Test-Path $NssmSourceExePath)) { Log-Error "Could not find nssm.exe at expected path: $NssmSourceExePath after extraction." # Fallback search for nssm.exe within the extracted directory $foundNssmFallback = Get-ChildItem -Path $TempExtractDir -Recurse -Filter "nssm.exe" | Where-Object { $_.FullName -like "*$NssmArchSubDir\nssm.exe" } | Select-Object -First 1 if ($foundNssmFallback) { Log-Warning "Found nssm.exe at $($foundNssmFallback.FullName) using fallback search. Using this." $NssmSourceExePath = $foundNssmFallback.FullName } else { Log-Error "nssm.exe ($NssmArchSubDir) still not found in $TempExtractDir. Please install nssm manually (from https://nssm.cc) and ensure it's in your PATH." exit 1 } } Copy-Item -Path $NssmSourceExePath -Destination $nssmExeToUse -Force $env:Path = "$($InstallDir);$($env:Path)" $nssmCmd = Get-Command nssm -ErrorAction SilentlyContinue # Re-check after adding to PATH if ($nssmCmd) { Log-Success "Downloaded nssm is now configured and available in PATH." } else { Log-Error "Failed to configure downloaded nssm in PATH from $nssmExeToUse. Please ensure $InstallDir is in your system PATH or nssm is installed globally." exit 1 } } catch { Log-Error "Failed to download or configure nssm: $_" Log-Error "Please install nssm manually from https://nssm.cc and ensure nssm.exe is in your PATH." exit 1 } finally { if (Test-Path $TempNssmZipPath) { Remove-Item $TempNssmZipPath -Force -ErrorAction SilentlyContinue } if (Test-Path $TempExtractDir) { Remove-Item $TempExtractDir -Recurse -Force -ErrorAction SilentlyContinue } } } # Final check that nssm is operational try { $nssmVersionOutput = nssm version 2>&1 } catch { Log-Error "nssm command failed to execute even after setup attempts. Please check the nssm installation and PATH. Error: $_" exit 1 } Log-Step "Installation configuration:" Log-Config "Service name: $ServiceName" Log-Config "Install directory: $InstallDir" Log-Config "GitHub proxy: $ProxyDisplay" Log-Config "Agent arguments: $($KomariArgs -join ' ')" if ($InstallVersion -ne "") { Log-Config "Specified agent version: $InstallVersion" } else { Log-Config "Agent version: Latest" } # Paths $BinaryName = "komari-agent-windows-$arch.exe" $AgentPath = Join-Path $InstallDir "komari-agent.exe" # Uninstall previous service and binary function Uninstall-Previous { Log-Step "Checking for existing service..." # Check if service exists using nssm status, as Get-Service might not work for nssm services if not properly registered $serviceStatus = nssm status $ServiceName 2>&1 if ($serviceStatus -notmatch "SERVICE_STOPPED" -and $serviceStatus -notmatch "does not exist") { Log-Info "Stopping service $ServiceName..." nssm stop $ServiceName 2>&1 | Out-Null } # Attempt to remove the service using nssm # We check if it exists first by trying to get its status. # nssm remove will succeed if the service exists, and fail otherwise. # We add confirm to avoid interactive prompts. $removeOutput = nssm remove $ServiceName confirm 2>&1 if ($LASTEXITCODE -eq 0) { } elseif ($removeOutput -match "Can't open service! (The specified service does not exist as an installed service.)" -or $removeOutput -match "No such service" -or $removeOutput -match "does not exist") { Log-Info "Service $ServiceName does not exist or was already removed." } else { # If nssm remove fails for other reasons, try sc.exe delete as a fallback for older installations $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($svc) { Stop-Service $ServiceName -Force -ErrorAction SilentlyContinue sc.exe delete $ServiceName | Out-Null } } if (Test-Path $AgentPath) { Log-Warning "Removing old binary..." Remove-Item $AgentPath -Force } } Uninstall-Previous function Get-LatestSnapshotVersion { param([Parameter(Mandatory = $true)][string]$AssetName) $ApiUrl = "https://api.github.com/repos/komari-monitor/komari-agent/releases?per_page=100" $ApiUrls = @($ApiUrl) if ($GitHubProxy -ne "") { $ApiUrls = @("$GitHubProxy/$ApiUrl", $ApiUrl) } for ($i = 0; $i -lt $ApiUrls.Count; $i++) { try { Log-Info "Fetching snapshot releases from GitHub API..." $releases = Invoke-RestMethod -Uri $ApiUrls[$i] -UseBasicParsing } catch { $releases = $null } if ($releases) { $latestSnapshot = $releases | Where-Object { $_.draft -eq $false -and $_.prerelease -eq $true -and $_.tag_name -like "Snapshot-*" -and (@($_.assets.name) -contains $AssetName) } | Sort-Object -Property @{ Expression = { [datetime]$_.published_at }; Descending = $true }, @{ Expression = { $_.tag_name }; Descending = $true } | Select-Object -First 1 if ($latestSnapshot) { return $latestSnapshot.tag_name } } if ($i -lt ($ApiUrls.Count - 1)) { Log-Warning "Failed to resolve snapshot releases through GitHub proxy, retrying directly." } } throw "No snapshot release contains asset $AssetName." } $versionToInstall = "" if ($InstallVersion -ne "") { Log-Info "Attempting to install specified version: $InstallVersion" if ($InstallVersion -ieq "snapshot") { Log-Info "Resolving the latest snapshot version..." try { $versionToInstall = Get-LatestSnapshotVersion -AssetName $BinaryName Log-Success "Latest snapshot version fetched: $versionToInstall" } catch { Log-Error "Failed to resolve the latest snapshot version: $_" exit 1 } } else { $versionToInstall = $InstallVersion } } else { $ApiUrl = "https://api.github.com/repos/komari-monitor/komari-agent/releases/latest" try { Log-Step "Fetching latest release version from GitHub API..." $release = Invoke-RestMethod -Uri $ApiUrl -UseBasicParsing $versionToInstall = $release.tag_name Log-Success "Latest version fetched: $versionToInstall" } catch { Log-Error "Failed to fetch latest version: $_" exit 1 } } Log-Success "Installing Komari Agent version: $versionToInstall" # Construct download URL $BinaryName = "komari-agent-windows-$arch.exe" $DownloadUrl = if ($GitHubProxy) { "$GitHubProxy/https://github.com/komari-monitor/komari-agent/releases/download/$versionToInstall/$BinaryName" } else { "https://github.com/komari-monitor/komari-agent/releases/download/$versionToInstall/$BinaryName" } # Download and install New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null Log-Info "URL: $DownloadUrl" try { Invoke-WebRequest -Uri $DownloadUrl -OutFile $AgentPath -UseBasicParsing } catch { Log-Error "Download failed: $_" exit 1 } Log-Success "Downloaded and saved to $AgentPath" # Register and start service Log-Step "Configuring Windows service with nssm..." $argString = $KomariArgs -join ' ' # The application path is passed WITHOUT literal quotes: PowerShell already # quotes native arguments containing spaces on the command line, while # embedded quotes made nssm store a broken Application path (komari-agent#118). nssm install $ServiceName $AgentPath $argString # Set display name and startup type using nssm nssm set $ServiceName DisplayName "Komari Agent Service" nssm set $ServiceName Start SERVICE_AUTO_START nssm set $ServiceName AppExit Default Restart nssm set $ServiceName AppRestartDelay 5000 # Start the service using nssm nssm start $ServiceName Log-Success "Service $ServiceName installed and started using nssm." Log-Success "Komari Agent installation completed!" Log-Config "Service name: $ServiceName" Log-Config "Arguments: $argString"