Add macOS and Windows agents + multi-platform install UI

- agent/skywatch-agent-macos.sh: Full macOS bash agent (launchd)
  - CPU via top -l 2, memory via vm_stat/sysctl, disk via df -P
  - Network via netstat -ib, uptime via kern.boottime, OS via sw_vers
- agent/install-agent-macos.sh: Installs macOS agent as launchd daemon
- agent/skywatch-agent-windows.ps1: PowerShell agent (Task Scheduler)
  - CPU via Win32_PerfRawData two-sample delta, memory via Win32_OperatingSystem
  - Disk via Get-PSDrive, network via Get-NetAdapterStatistics
- agent/install-agent-windows.ps1: Installs Windows agent via Task Scheduler
- server/index.js: New routes for macOS and Windows agent scripts/installers
- client/src/Agents.js: Multi-platform install panel with Linux/macOS/Windows tabs
- client/src/Agents.css: Platform tab button styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-11 16:54:50 -04:00
1 parent 784e539af3
commit 13b3e3612b
16 files changed
+1009 -34

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
#!/bin/bash
# SkyWatch macOS Agent Installer v1.0.0
# Installs the agent as a launchd daemon (runs at boot, requires sudo)
#
# Usage:
# sudo bash install-agent-macos.sh --server-url https://skywatch.example.com
#
# Options:
# --server-url <url> SkyWatch server URL (required)
# --interval <secs> Reporting interval in seconds (default: 60)
# --name <label> Agent display name (default: hostname)
# --registration-key <key> Optional registration key
# --uninstall Remove the agent
set -e
PLIST_LABEL="us.necloud.skywatch-agent"
PLIST_PATH="/Library/LaunchDaemons/${PLIST_LABEL}.plist"
AGENT_SCRIPT="/usr/local/bin/skywatch-agent-macos.sh"
CONFIG_FILE="/etc/skywatch-agent-macos.conf"
LOG_FILE="/var/log/skywatch-agent.log"
SERVER_URL=""
INTERVAL=60
AGENT_NAME="$(hostname -f 2>/dev/null || hostname)"
REGISTRATION_KEY=""
UNINSTALL=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--server-url) SERVER_URL="$2"; shift 2 ;;
--interval) INTERVAL="$2"; shift 2 ;;
--name) AGENT_NAME="$2"; shift 2 ;;
--registration-key) REGISTRATION_KEY="$2"; shift 2 ;;
--uninstall) UNINSTALL=true; shift ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
step() { echo "==> $*"; }
ok() { echo " OK: $*"; }
# ── Uninstall ──────────────────────────────────────────────────────────────────
if [ "$UNINSTALL" = true ]; then
step "Stopping and removing SkyWatch agent ..."
launchctl unload "$PLIST_PATH" 2>/dev/null || true
rm -f "$PLIST_PATH" "$AGENT_SCRIPT" "$CONFIG_FILE"
ok "SkyWatch agent removed."
ok "Log file kept at ${LOG_FILE} — remove manually if desired."
exit 0
fi
# ── Require root ───────────────────────────────────────────────────────────────
if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: This installer must be run as root: sudo bash $0 $*"
exit 1
fi
if [ -z "$SERVER_URL" ]; then
echo "ERROR: --server-url is required."
echo "Usage: sudo bash install-agent-macos.sh --server-url https://skywatch.example.com"
exit 1
fi
# ── Install ────────────────────────────────────────────────────────────────────
step "Downloading agent script from ${SERVER_URL} ..."
curl -fsSL "${SERVER_URL}/api/agents/script-macos" -o "$AGENT_SCRIPT"
chmod +x "$AGENT_SCRIPT"
ok "Agent script installed to ${AGENT_SCRIPT}"
step "Writing configuration ..."
cat > "$CONFIG_FILE" << CONF
SKYWATCH_URL="${SERVER_URL}"
AGENT_TOKEN=""
REPORT_INTERVAL="${INTERVAL}"
AGENT_NAME="${AGENT_NAME}"
REGISTRATION_KEY="${REGISTRATION_KEY}"
CONF
chmod 600 "$CONFIG_FILE"
ok "Config written to ${CONFIG_FILE}"
step "Installing launchd daemon ..."
cat > "$PLIST_PATH" << PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${PLIST_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${AGENT_SCRIPT}</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>CONFIG_FILE</key>
<string>${CONFIG_FILE}</string>
<key>LOG_FILE</key>
<string>${LOG_FILE}</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>${LOG_FILE}</string>
<key>StandardErrorPath</key>
<string>${LOG_FILE}</string>
</dict>
</plist>
PLIST
chmod 644 "$PLIST_PATH"
ok "Daemon plist written to ${PLIST_PATH}"
step "Loading daemon ..."
launchctl unload "$PLIST_PATH" 2>/dev/null || true
launchctl load -w "$PLIST_PATH"
echo ""
echo "✅ SkyWatch Agent installed and started!"
echo " Logs: tail -f ${LOG_FILE}"
echo " Status: launchctl list | grep skywatch"
echo " Uninstall: sudo bash install-agent-macos.sh --uninstall"
echo ""
echo "The agent will register with ${SERVER_URL} and appear in your"
echo "SkyWatch dashboard within a minute."
+144
View File
@@ -0,0 +1,144 @@
#Requires -RunAsAdministrator
#Requires -Version 5.1
<#
.SYNOPSIS
SkyWatch Windows Agent Installer v1.0.0
.DESCRIPTION
Downloads and installs the SkyWatch agent as a Windows Task Scheduler job
that starts at boot and runs as SYSTEM.
.PARAMETER ServerUrl
SkyWatch server URL (required), e.g. https://skywatch.necloud.us
.PARAMETER AgentName
Display name for this agent (default: computer name)
.PARAMETER Interval
Reporting interval in seconds (default: 60)
.PARAMETER RegistrationKey
Optional registration key if server requires one
.PARAMETER Uninstall
Remove the agent and its scheduled task
.EXAMPLE
.\install-agent-windows.ps1 -ServerUrl https://skywatch.necloud.us
.EXAMPLE
.\install-agent-windows.ps1 -Uninstall
#>
[CmdletBinding()]
param(
[string]$ServerUrl,
[string]$AgentName,
[int] $Interval = 60,
[string]$RegistrationKey,
[switch]$Uninstall
)
$TaskName = "SkyWatch Agent"
$InstallDir = "C:\ProgramData\SkyWatch"
$AgentScript = "$InstallDir\skywatch-agent-windows.ps1"
$ConfigFile = "$InstallDir\agent.conf"
function Write-Step { param([string]$Msg) Write-Host "==> $Msg" -ForegroundColor Cyan }
function Write-OK { param([string]$Msg) Write-Host " $Msg" -ForegroundColor Green }
function Write-Err { param([string]$Msg) Write-Host " ERROR: $Msg" -ForegroundColor Red; exit 1 }
# ── Uninstall ──────────────────────────────────────────────────────────────────
if ($Uninstall) {
Write-Step "Removing SkyWatch Agent ..."
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
if (Test-Path $InstallDir) { Remove-Item $InstallDir -Recurse -Force }
Write-OK "SkyWatch Agent removed."
exit 0
}
# ── Validate ───────────────────────────────────────────────────────────────────
if (-not $ServerUrl) {
Write-Err "ServerUrl is required.`nExample: .\install-agent-windows.ps1 -ServerUrl https://skywatch.necloud.us"
}
# ── Install ────────────────────────────────────────────────────────────────────
Write-Step "Creating install directory ..."
if (-not (Test-Path $InstallDir)) {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
Write-OK $InstallDir
Write-Step "Downloading agent script from $ServerUrl ..."
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri "$ServerUrl/api/agents/script-windows" `
-OutFile $AgentScript -UseBasicParsing -TimeoutSec 30
Write-OK "Saved to $AgentScript"
} catch {
Write-Err "Download failed: $_"
}
Write-Step "Writing configuration ..."
$name = if ($AgentName) { $AgentName } else { $env:COMPUTERNAME }
@"
SKYWATCH_URL="$ServerUrl"
AGENT_TOKEN=""
REPORT_INTERVAL=$Interval
AGENT_NAME="$name"
REGISTRATION_KEY="$RegistrationKey"
"@ | Set-Content $ConfigFile -Encoding UTF8
Write-OK "Config saved to $ConfigFile"
# Allow scripts to run (per-machine, won't affect user policy)
Write-Step "Setting PowerShell execution policy ..."
try {
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force -ErrorAction Stop
Write-OK "RemoteSigned (LocalMachine)"
} catch {
Write-Host " (Could not set policy — you may need to run: Set-ExecutionPolicy RemoteSigned)" -ForegroundColor Yellow
}
Write-Step "Registering scheduled task ..."
# Remove any previous install
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
$psArgs = "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$AgentScript`" -ConfigFile `"$ConfigFile`""
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $psArgs
$triggers = @(
(New-ScheduledTaskTrigger -AtStartup),
(New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(10) `
-RepetitionInterval (New-TimeSpan -Minutes 5)) # restart every 5 min if it exits
)
$settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Hours 0) `
-RestartCount 5 `
-RestartInterval (New-TimeSpan -Minutes 1) `
-MultipleInstances IgnoreNew `
-StartWhenAvailable
$principal = New-ScheduledTaskPrincipal `
-UserId "SYSTEM" `
-LogonType ServiceAccount `
-RunLevel Highest
Register-ScheduledTask `
-TaskName $TaskName `
-Action $action `
-Trigger $triggers `
-Settings $settings `
-Principal $principal `
-Force | Out-Null
Write-OK "Task '$TaskName' created"
Write-Step "Starting agent ..."
Start-ScheduledTask -TaskName $TaskName
Start-Sleep -Seconds 3
$state = (Get-ScheduledTask -TaskName $TaskName).State
Write-OK "Task state: $state"
Write-Host ""
Write-Host "✅ SkyWatch Agent installed!" -ForegroundColor Green
Write-Host " Logs: Get-Content '$InstallDir\agent.log' -Wait"
Write-Host " Status: Get-ScheduledTask -TaskName '$TaskName'"
Write-Host " Uninstall: .\install-agent-windows.ps1 -Uninstall"
Write-Host ""
Write-Host "The agent will register with $ServerUrl and appear in your"
Write-Host "SkyWatch dashboard within a minute."
+287
View File
@@ -0,0 +1,287 @@
#!/bin/bash
# SkyWatch macOS Agent v1.0.0
# Collects system metrics and sends to SkyWatch monitoring server
#
# Requirements: macOS 10.15+, bash 3.2+, curl
# Run via launchd (see install-agent-macos.sh) or: bash skywatch-agent-macos.sh
VERSION="1.0.0"
CONFIG_FILE="${CONFIG_FILE:-/etc/skywatch-agent-macos.conf}"
LOG_FILE="${LOG_FILE:-/var/log/skywatch-agent.log}"
# Fall back to home dir if system paths aren't writable
if [ ! -w "$(dirname "$CONFIG_FILE")" ] 2>/dev/null; then
CONFIG_FILE="$HOME/.skywatch-agent-macos.conf"
LOG_FILE="$HOME/.skywatch-agent.log"
fi
log() {
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "")
local msg="[$ts] $*"
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
echo "$msg"
}
load_config() {
if [ -f "$CONFIG_FILE" ]; then
# shellcheck disable=SC1090
source "$CONFIG_FILE"
fi
}
save_config() {
cat > "$CONFIG_FILE" << CONF
SKYWATCH_URL="${SKYWATCH_URL:-}"
AGENT_TOKEN="${AGENT_TOKEN:-}"
REPORT_INTERVAL="${REPORT_INTERVAL:-60}"
AGENT_NAME="${AGENT_NAME:-}"
REGISTRATION_KEY="${REGISTRATION_KEY:-}"
CONF
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
}
# CPU usage % — two samples from top, 1 second apart
get_cpu_percent() {
local line idle
line=$(top -l 2 -n 0 2>/dev/null | grep "^CPU usage:" | tail -1)
# "CPU usage: 8.46% user, 11.45% sys, 80.08% idle"
idle=$(echo "$line" | awk '{
for (i = 1; i <= NF; i++) {
if ($i ~ /idle/) {
v = $(i - 1); gsub(/%/, "", v); print v; exit
}
}
}')
[ -z "$idle" ] && idle=0
awk -v i="$idle" 'BEGIN { printf "%.1f", 100 - i }'
}
# Memory stats in MB — uses vm_stat + sysctl
get_memory() {
local total_bytes vm_out page_size pages_free pages_inactive pages_spec
total_bytes=$(sysctl -n hw.memsize 2>/dev/null || echo 0)
vm_out=$(vm_stat 2>/dev/null)
# Page size from vm_stat header (4096 on Intel, 16384 on Apple Silicon)
page_size=$(echo "$vm_out" | awk '/page size of/ {
for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/ && $(i+1) == "bytes") { print $i; exit }
}')
[ -z "$page_size" ] && page_size=$(sysctl -n hw.pagesize 2>/dev/null || echo 4096)
pages_free=$(echo "$vm_out" | awk '/^Pages free:/ { gsub(/\./, "", $NF); print $NF+0 }')
pages_inactive=$(echo "$vm_out" | awk '/^Pages inactive:/ { gsub(/\./, "", $NF); print $NF+0 }')
pages_spec=$(echo "$vm_out" | awk '/^Pages speculative:/ { gsub(/\./, "", $NF); print $NF+0 }')
awk -v total="$total_bytes" -v page="$page_size" \
-v free="${pages_free:-0}" -v inact="${pages_inactive:-0}" -v spec="${pages_spec:-0}" \
'BEGIN {
total_mb = int(total / 1048576)
avail_mb = int((free + inact + spec) * page / 1048576)
used_mb = total_mb - avail_mb
if (used_mb < 0) used_mb = 0
pct = (total_mb > 0) ? used_mb * 100.0 / total_mb : 0
printf "%d %d %.1f", total_mb, used_mb, pct
}'
}
# Disk info as JSON — real mounted volumes, skips macOS internal system volumes
get_disk_json() {
df -P -k 2>/dev/null | tail -n +2 | awk '
$6 ~ /^\/System\/Volumes\// { next }
$6 ~ /^\/private\/var\/vm/ { next }
NF >= 6 {
pct = $5; sub(/%/, "", pct)
mp = $6
gsub(/\\/, "\\\\", mp); gsub(/"/, "\\\"", mp)
if (c > 0) printf ","
printf "{\"path\":\"%s\",\"total_mb\":%d,\"used_mb\":%d,\"percent\":%d}",
mp, int($2/1024), int($3/1024), int(pct)
c++
}
BEGIN { printf "[" }
END { printf "]" }
'
}
# Cumulative network bytes — hardware-level rows from netstat -ib, no loopback
get_net_bytes() {
netstat -ib 2>/dev/null | awk '
/Name/ { next }
/<Link#/ && !/^lo/ { recv += $7; sent += $10 }
END { printf "%d %d", sent, recv }
' || echo "0 0"
}
# 1/5/15-min load averages from sysctl
get_load() {
sysctl -n vm.loadavg 2>/dev/null \
| awk '{ printf "%s %s %s", $2, $3, $4 }' \
|| echo "0.00 0.00 0.00"
}
# System uptime in seconds from boot time
get_uptime_seconds() {
local boot_sec now_sec
# kern.boottime: { sec = NNNN, usec = N }
boot_sec=$(sysctl -n kern.boottime 2>/dev/null \
| awk '{ for (i=1;i<=NF;i++) if ($i=="sec") { v=$(i+2); gsub(/[^0-9]/,"",v); print v; exit } }')
now_sec=$(date +%s)
if [ -n "$boot_sec" ] && [ "$boot_sec" -gt 0 ] 2>/dev/null; then
echo $((now_sec - boot_sec))
else
echo 0
fi
}
# Running process count
get_process_count() {
ps -ax 2>/dev/null | tail -n +2 | wc -l | tr -d ' \n'
}
# Primary non-loopback IPv4
get_ip_address() {
local iface
iface=$(route -n get default 2>/dev/null | awk '/interface:/ { print $2 }')
if [ -n "$iface" ]; then
ipconfig getifaddr "$iface" 2>/dev/null \
|| ifconfig "$iface" 2>/dev/null | awk '/inet / { print $2; exit }'
fi
}
# macOS version string
get_os_info() {
local name ver
name=$(sw_vers -productName 2>/dev/null || echo "macOS")
ver=$(sw_vers -productVersion 2>/dev/null || echo "")
echo "${name} ${ver}"
}
# Register with server; saves token to config on success
register_agent() {
local name="${AGENT_NAME:-$(hostname -f 2>/dev/null || hostname)}"
local hostname_val
hostname_val=$(hostname -f 2>/dev/null || hostname)
local ip
ip=$(get_ip_address || echo "")
local os
os=$(get_os_info)
local esc_name esc_host esc_os
esc_name="${name//\"/\\\"}"
esc_host="${hostname_val//\"/\\\"}"
esc_os="${os//\"/\\\"}"
log "Registering agent '${name}' with ${SKYWATCH_URL} ..."
local -a curl_args
curl_args=(-sf -m 30 -X POST -H 'Content-Type: application/json')
[ -n "${REGISTRATION_KEY:-}" ] && curl_args+=(-H "X-Registration-Key: ${REGISTRATION_KEY}")
curl_args+=(-d "{\"name\":\"${esc_name}\",\"hostname\":\"${esc_host}\",\"ip_address\":\"${ip}\",\"os_info\":\"${esc_os}\"}")
curl_args+=("${SKYWATCH_URL}/api/agents/register")
local response
response=$(curl "${curl_args[@]}" 2>&1) || {
log "ERROR: Could not reach ${SKYWATCH_URL}"
return 1
}
local token
token=$(echo "$response" | grep -o '"token":"[^"]*"' | sed 's/"token":"//; s/"$//')
if [ -z "$token" ]; then
log "ERROR: Registration failed. Server said: ${response}"
return 1
fi
AGENT_TOKEN="$token"
save_config
log "Registered. Token stored in ${CONFIG_FILE}"
}
# Collect and POST metrics
report_metrics() {
local cpu
cpu=$(get_cpu_percent) # includes ~1s sleep inside top
local mem_total mem_used mem_percent
read -r mem_total mem_used mem_percent <<< "$(get_memory)"
local disk
disk=$(get_disk_json)
local net_sent net_recv
read -r net_sent net_recv <<< "$(get_net_bytes)"
local load1 load5 load15
read -r load1 load5 load15 <<< "$(get_load)"
local uptime_secs
uptime_secs=$(get_uptime_seconds)
local procs
procs=$(get_process_count)
local ip
ip=$(get_ip_address || echo "")
cpu="${cpu:-0.0}"; mem_total="${mem_total:-0}"; mem_used="${mem_used:-0}"
mem_percent="${mem_percent:-0.0}"; disk="${disk:-[]}"; net_sent="${net_sent:-0}"
net_recv="${net_recv:-0}"; load1="${load1:-0.00}"; load5="${load5:-0.00}"
load15="${load15:-0.00}"; uptime_secs="${uptime_secs:-0}"; procs="${procs:-0}"
local payload
payload=$(printf \
'{"cpu_percent":%s,"mem_total":%d,"mem_used":%d,"mem_percent":%s,"disk":%s,"load_1":%s,"load_5":%s,"load_15":%s,"uptime_seconds":%d,"process_count":%d,"net_bytes_sent":%s,"net_bytes_recv":%s,"ip_address":"%s"}' \
"$cpu" "$mem_total" "$mem_used" "$mem_percent" \
"$disk" "$load1" "$load5" "$load15" \
"$uptime_secs" "$procs" "$net_sent" "$net_recv" "${ip}")
local response
response=$(curl -sf -m 15 \
-X POST \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${AGENT_TOKEN}" \
-d "$payload" \
"${SKYWATCH_URL}/api/agents/report" 2>&1) || {
log "WARNING: Failed to send metrics (network error)"
return 0
}
if echo "$response" | grep -q '"ok":true'; then
log "OK cpu=${cpu}% mem=${mem_used}/${mem_total}MB load=${load1}"
else
log "WARNING: Server rejected metrics: ${response}"
fi
}
main() {
load_config
if [ -z "${SKYWATCH_URL:-}" ]; then
echo "ERROR: SKYWATCH_URL is not set."
echo "Create ${CONFIG_FILE} or use the installer:"
echo " sudo bash install-agent-macos.sh --server-url https://<your-server>"
exit 1
fi
if [ -z "${AGENT_TOKEN:-}" ]; then
if ! register_agent; then
log "Retrying registration in 60 s..."
sleep 60
register_agent || exit 1
fi
fi
local interval="${REPORT_INTERVAL:-60}"
log "SkyWatch Agent v${VERSION} started (macOS) — server=${SKYWATCH_URL} interval=${interval}s"
while true; do
report_metrics
local sleep_time=$(( interval - 1 ))
[ "$sleep_time" -lt 1 ] && sleep_time=1
sleep "$sleep_time"
done
}
main "$@"
+257
View File
@@ -0,0 +1,257 @@
#Requires -Version 5.1
<#
.SYNOPSIS
SkyWatch Windows Agent v1.0.0
.DESCRIPTION
Collects system metrics and sends to SkyWatch monitoring server.
Metrics: CPU%, Memory, Disk, Network I/O, Process count, Uptime, OS info.
Run via Task Scheduler (installed by install-agent-windows.ps1).
.PARAMETER ConfigFile
Path to the configuration file (default: C:\ProgramData\SkyWatch\agent.conf)
#>
param(
[string]$ConfigFile = "C:\ProgramData\SkyWatch\agent.conf"
)
$VERSION = "1.0.0"
$LogFile = "C:\ProgramData\SkyWatch\agent.log"
$InstallDir = Split-Path $ConfigFile
# ── Logging ───────────────────────────────────────────────────────────────────
function Write-Log {
param([string]$Message)
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$ts] $Message"
Write-Host $line
try { Add-Content -Path $LogFile -Value $line -ErrorAction Stop } catch {}
}
# ── Config ────────────────────────────────────────────────────────────────────
function Load-Config {
$script:SKYWATCH_URL = ""
$script:AGENT_TOKEN = ""
$script:REPORT_INTERVAL = 60
$script:AGENT_NAME = ""
$script:REGISTRATION_KEY = ""
if (Test-Path $ConfigFile) {
Get-Content $ConfigFile | ForEach-Object {
if ($_ -match '^([A-Z_]+)="?(.*?)"?\s*$') {
$key = $matches[1]; $val = $matches[2]
switch ($key) {
"SKYWATCH_URL" { $script:SKYWATCH_URL = $val }
"AGENT_TOKEN" { $script:AGENT_TOKEN = $val }
"REPORT_INTERVAL" { $script:REPORT_INTERVAL = [int]$val }
"AGENT_NAME" { $script:AGENT_NAME = $val }
"REGISTRATION_KEY" { $script:REGISTRATION_KEY = $val }
}
}
}
}
}
function Save-Config {
if (-not (Test-Path $InstallDir)) {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
@"
SKYWATCH_URL="$script:SKYWATCH_URL"
AGENT_TOKEN="$script:AGENT_TOKEN"
REPORT_INTERVAL=$script:REPORT_INTERVAL
AGENT_NAME="$script:AGENT_NAME"
REGISTRATION_KEY="$script:REGISTRATION_KEY"
"@ | Set-Content $ConfigFile -Encoding UTF8
}
# ── Metrics ───────────────────────────────────────────────────────────────────
function Get-CpuPercent {
try {
# Two raw counter samples 1 second apart for accurate delta
$s1 = Get-CimInstance -ClassName Win32_PerfRawData_PerfOS_Processor -Filter "Name='_Total'"
Start-Sleep -Seconds 1
$s2 = Get-CimInstance -ClassName Win32_PerfRawData_PerfOS_Processor -Filter "Name='_Total'"
$idleDelta = $s2.PercentIdleTime - $s1.PercentIdleTime
$totalDelta = $s2.Timestamp_Sys100NS - $s1.Timestamp_Sys100NS
if ($totalDelta -gt 0) {
$idlePct = ($idleDelta / $totalDelta) * 100
return [Math]::Round(100 - $idlePct, 1)
}
} catch {}
# Fallback: WMI LoadPercentage (less accurate but always available)
try {
$avg = (Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
return [Math]::Round($avg, 1)
} catch { return 0.0 }
}
function Get-MemoryStats {
try {
$os = Get-CimInstance Win32_OperatingSystem
$totalMB = [long][Math]::Round($os.TotalVisibleMemorySize / 1024)
$freeMB = [long][Math]::Round($os.FreePhysicalMemory / 1024)
$usedMB = $totalMB - $freeMB
$pct = if ($totalMB -gt 0) { [Math]::Round($usedMB * 100.0 / $totalMB, 1) } else { 0.0 }
return @{ total = $totalMB; used = $usedMB; percent = $pct }
} catch {
return @{ total = 0; used = 0; percent = 0.0 }
}
}
function Get-DiskStats {
$disks = [System.Collections.Generic.List[hashtable]]::new()
try {
Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -match '^[A-Za-z]:\\' } | ForEach-Object {
$total = $_.Used + $_.Free
$totalMB = [long][Math]::Round($total / 1MB)
$usedMB = [long][Math]::Round($_.Used / 1MB)
$pct = if ($total -gt 0) { [int][Math]::Round($_.Used * 100.0 / $total) } else { 0 }
$disks.Add(@{ path = $_.Root; total_mb = $totalMB; used_mb = $usedMB; percent = $pct })
}
} catch {}
return $disks
}
function Get-NetBytes {
try {
$up = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | Select-Object -ExpandProperty Name
if ($up) {
$stats = Get-NetAdapterStatistics -Name $up -ErrorAction SilentlyContinue
$sent = ($stats | Measure-Object -Property SentBytes -Sum).Sum
$recv = ($stats | Measure-Object -Property ReceivedBytes -Sum).Sum
return @{ sent = [long]$sent; recv = [long]$recv }
}
} catch {}
return @{ sent = 0L; recv = 0L }
}
function Get-UptimeSeconds {
try {
$boot = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
return [long](New-TimeSpan -Start $boot -End (Get-Date)).TotalSeconds
} catch { return 0L }
}
function Get-OsInfo {
try { return (Get-CimInstance Win32_OperatingSystem).Caption.Trim() }
catch { return "Windows" }
}
function Get-PrimaryIP {
try {
$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
Sort-Object RouteMetric | Select-Object -First 1
if ($route) {
$ip = Get-NetIPAddress -InterfaceIndex $route.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -notmatch '^169\.254\.' } |
Select-Object -First 1 -ExpandProperty IPAddress
return "$ip"
}
} catch {}
return ""
}
# ── Registration ──────────────────────────────────────────────────────────────
function Register-Agent {
$name = if ($script:AGENT_NAME) { $script:AGENT_NAME } else { $env:COMPUTERNAME }
$hostname = $env:COMPUTERNAME
$ip = Get-PrimaryIP
$os = Get-OsInfo
Write-Log "Registering agent '$name' with $script:SKYWATCH_URL ..."
$body = @{ name = $name; hostname = $hostname; ip_address = $ip; os_info = $os } | ConvertTo-Json
$headers = @{ 'Content-Type' = 'application/json' }
if ($script:REGISTRATION_KEY) { $headers['X-Registration-Key'] = $script:REGISTRATION_KEY }
try {
$resp = Invoke-RestMethod -Uri "$script:SKYWATCH_URL/api/agents/register" `
-Method POST -Body $body -Headers $headers -TimeoutSec 30
if ($resp.token) {
$script:AGENT_TOKEN = $resp.token
Save-Config
Write-Log "Registered. Token stored in $ConfigFile"
return $true
}
} catch { Write-Log "ERROR: Registration failed: $_" }
return $false
}
# ── Metrics report ────────────────────────────────────────────────────────────
function Send-Metrics {
$cpu = Get-CpuPercent # includes 1s sleep
$mem = Get-MemoryStats
$disks = Get-DiskStats
$net = Get-NetBytes
$uptime = Get-UptimeSeconds
$procs = try { (Get-Process -ErrorAction SilentlyContinue).Count } catch { 0 }
$ip = Get-PrimaryIP
$payload = [ordered]@{
cpu_percent = $cpu
mem_total = $mem.total
mem_used = $mem.used
mem_percent = $mem.percent
disk = @($disks)
load_1 = 0.0
load_5 = 0.0
load_15 = 0.0
uptime_seconds = $uptime
process_count = $procs
net_bytes_sent = $net.sent
net_bytes_recv = $net.recv
ip_address = $ip
} | ConvertTo-Json -Depth 3
try {
$resp = Invoke-RestMethod `
-Uri "$script:SKYWATCH_URL/api/agents/report" `
-Method POST `
-Body $payload `
-Headers @{ 'Content-Type' = 'application/json'; 'Authorization' = "Bearer $script:AGENT_TOKEN" } `
-TimeoutSec 15
if ($resp.ok) {
Write-Log "OK cpu=$($cpu)% mem=$($mem.used)/$($mem.total)MB procs=$procs"
} else {
Write-Log "WARNING: Server rejected metrics"
}
} catch {
Write-Log "WARNING: Failed to send metrics: $_"
}
}
# ── Main ──────────────────────────────────────────────────────────────────────
# Ensure log directory exists
if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null }
Load-Config
if (-not $script:SKYWATCH_URL) {
Write-Error "SKYWATCH_URL is not set. Edit $ConfigFile or re-run the installer."
exit 1
}
if (-not $script:AGENT_TOKEN) {
if (-not (Register-Agent)) {
Write-Log "Retrying registration in 60s ..."
Start-Sleep -Seconds 60
if (-not (Register-Agent)) { exit 1 }
}
}
$interval = if ($script:REPORT_INTERVAL -gt 0) { $script:REPORT_INTERVAL } else { 60 }
Write-Log "SkyWatch Agent v$VERSION started (Windows) — server=$script:SKYWATCH_URL interval=${interval}s"
while ($true) {
Send-Metrics
Start-Sleep -Seconds ($interval - 1) # -1 accounts for the 1s CPU sample sleep
}