diff --git a/agent/skywatch-agent-macos.sh b/agent/skywatch-agent-macos.sh index face471..7d3bb6e 100644 --- a/agent/skywatch-agent-macos.sh +++ b/agent/skywatch-agent-macos.sh @@ -255,6 +255,51 @@ report_metrics() { fi } +poll_commands() { + local resp http_code + resp=$(curl -sf -m 10 \ + -H "Authorization: Bearer ${AGENT_TOKEN}" \ + -w '\n__HTTP_CODE__%{http_code}' \ + "${SKYWATCH_URL}/api/agents/commands/pending" 2>/dev/null) || return 0 + + http_code=$(echo "$resp" | grep -o '__HTTP_CODE__[0-9]*' | tail -1 | grep -o '[0-9]*') + resp=$(echo "$resp" | sed '/__HTTP_CODE__/d') + + [ "$http_code" = "204" ] && return 0 + [ "$http_code" != "200" ] && return 0 + + local cmd_id cmd_str + cmd_id=$(echo "$resp" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') + cmd_str=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('command',''))" 2>/dev/null \ + || echo "$resp" | sed -n 's/.*"command":"\([^"]*\)".*/\1/p') + + [ -z "$cmd_id" ] || [ -z "$cmd_str" ] && return 0 + + log "Running command #${cmd_id}: ${cmd_str}" + + local tmp_out exit_code + tmp_out=$(mktemp) + gtimeout 60 bash -c "$cmd_str" > "$tmp_out" 2>&1 || timeout 60 bash -c "$cmd_str" > "$tmp_out" 2>&1 + exit_code=$? + + local tmp_json + tmp_json=$(mktemp) + python3 - "$tmp_out" "$exit_code" > "$tmp_json" 2>/dev/null << 'PYEOF' +import sys, json +with open(sys.argv[1]) as f: + output = f.read(131072) +print(json.dumps({"output": output, "exit_code": int(sys.argv[2])})) +PYEOF + + curl -sf -m 15 -X POST \ + -H "Authorization: Bearer ${AGENT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "@${tmp_json}" \ + "${SKYWATCH_URL}/api/agents/commands/${cmd_id}/result" >/dev/null 2>&1 || true + + rm -f "$tmp_out" "$tmp_json" +} + main() { load_config @@ -278,6 +323,7 @@ main() { while true; do report_metrics + poll_commands local sleep_time=$(( interval - 1 )) [ "$sleep_time" -lt 1 ] && sleep_time=1 sleep "$sleep_time" diff --git a/agent/skywatch-agent-windows.ps1 b/agent/skywatch-agent-windows.ps1 index db61173..d449655 100644 --- a/agent/skywatch-agent-windows.ps1 +++ b/agent/skywatch-agent-windows.ps1 @@ -228,6 +228,61 @@ function Send-Metrics { } } +# ── Command execution ───────────────────────────────────────────────────────── + +function Poll-Commands { + try { + $resp = Invoke-WebRequest ` + -Uri "$script:SKYWATCH_URL/api/agents/commands/pending" ` + -Headers @{ 'Authorization' = "Bearer $script:AGENT_TOKEN" } ` + -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop + + if ($resp.StatusCode -eq 204) { return } + if ($resp.StatusCode -ne 200) { return } + + $data = $resp.Content | ConvertFrom-Json + $cmdId = $data.id + $cmdStr = $data.command + if (-not $cmdId -or -not $cmdStr) { return } + + Write-Log "Running command #${cmdId}: ${cmdStr}" + + $output = "" + $exitCode = -1 + try { + $proc = Start-Process -FilePath "powershell.exe" ` + -ArgumentList "-NonInteractive", "-Command", $cmdStr ` + -Wait -PassThru -NoNewWindow ` + -RedirectStandardOutput "$env:TEMP\sw_cmd_out.txt" ` + -RedirectStandardError "$env:TEMP\sw_cmd_err.txt" ` + -ErrorAction Stop + $exitCode = $proc.ExitCode + $stdout = if (Test-Path "$env:TEMP\sw_cmd_out.txt") { Get-Content "$env:TEMP\sw_cmd_out.txt" -Raw } else { "" } + $stderr = if (Test-Path "$env:TEMP\sw_cmd_err.txt") { Get-Content "$env:TEMP\sw_cmd_err.txt" -Raw } else { "" } + $output = ($stdout + $stderr).Substring(0, [Math]::Min(($stdout + $stderr).Length, 131072)) + } catch { + $output = "ERROR: $_" + $exitCode = 1 + } finally { + Remove-Item "$env:TEMP\sw_cmd_out.txt", "$env:TEMP\sw_cmd_err.txt" -ErrorAction SilentlyContinue + } + + $payload = @{ output = $output; exit_code = $exitCode } | ConvertTo-Json -Compress + Invoke-RestMethod ` + -Uri "$script:SKYWATCH_URL/api/agents/commands/$cmdId/result" ` + -Method POST ` + -Body $payload ` + -Headers @{ 'Content-Type' = 'application/json'; 'Authorization' = "Bearer $script:AGENT_TOKEN" } ` + -TimeoutSec 15 -ErrorAction SilentlyContinue | Out-Null + + } catch [System.Net.WebException] { + # 204 No Content arrives as an exception in some PS versions — that's fine + if ($_.Exception.Response -and $_.Exception.Response.StatusCode -eq 204) { return } + } catch { + # Network errors — silently ignore + } +} + # ── Main ────────────────────────────────────────────────────────────────────── # Ensure log directory exists @@ -253,5 +308,6 @@ Write-Log "SkyWatch Agent v$VERSION started (Windows) — server=$script:SKYWATC while ($true) { Send-Metrics + Poll-Commands Start-Sleep -Seconds ($interval - 1) # -1 accounts for the 1s CPU sample sleep } diff --git a/agent/skywatch-agent.sh b/agent/skywatch-agent.sh index 3376a1d..b9f1673 100755 --- a/agent/skywatch-agent.sh +++ b/agent/skywatch-agent.sh @@ -263,6 +263,59 @@ report_metrics() { fi } +# Poll for and execute one pending command from the server +poll_commands() { + local resp http_code + resp=$(curl -sf -m 10 \ + -H "Authorization: Bearer ${AGENT_TOKEN}" \ + -w '\n__HTTP_CODE__%{http_code}' \ + "${SKYWATCH_URL}/api/agents/commands/pending" 2>/dev/null) || return 0 + + http_code=$(echo "$resp" | grep -o '__HTTP_CODE__[0-9]*' | tail -1 | grep -o '[0-9]*') + resp=$(echo "$resp" | sed '/__HTTP_CODE__/d') + + # 204 = no pending commands + [ "$http_code" = "204" ] && return 0 + [ "$http_code" != "200" ] && return 0 + + local cmd_id cmd_str + cmd_id=$(echo "$resp" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') + # Extract command value (handles escaped quotes) + cmd_str=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('command',''))" 2>/dev/null \ + || echo "$resp" | sed -n 's/.*"command":"\([^"]*\)".*/\1/p') + + [ -z "$cmd_id" ] || [ -z "$cmd_str" ] && return 0 + + log "Running command #${cmd_id}: ${cmd_str}" + + # Execute with 60 s timeout, capture output, cap at 128 KB + local tmp_out exit_code + tmp_out=$(mktemp) + timeout 60 bash -c "$cmd_str" > "$tmp_out" 2>&1 + exit_code=$? + + # Truncate to 128 KB + truncate -s -"$(( $(wc -c < "$tmp_out") > 131072 ? $(wc -c < "$tmp_out") - 131072 : 0 ))" "$tmp_out" 2>/dev/null || true + + # Build JSON payload via python (handles all escaping correctly) + local tmp_json + tmp_json=$(mktemp) + python3 - "$tmp_out" "$exit_code" > "$tmp_json" 2>/dev/null << 'PYEOF' +import sys, json +with open(sys.argv[1]) as f: + output = f.read(131072) +print(json.dumps({"output": output, "exit_code": int(sys.argv[2])})) +PYEOF + + curl -sf -m 15 -X POST \ + -H "Authorization: Bearer ${AGENT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "@${tmp_json}" \ + "${SKYWATCH_URL}/api/agents/commands/${cmd_id}/result" >/dev/null 2>&1 || true + + rm -f "$tmp_out" "$tmp_json" +} + main() { load_config @@ -291,6 +344,7 @@ main() { while true; do report_metrics + poll_commands # get_cpu_percent already consumed ~1 s; subtract it from the sleep local sleep_time=$(( interval - 1 )) [ "$sleep_time" -lt 1 ] && sleep_time=1 diff --git a/client/build/asset-manifest.json b/client/build/asset-manifest.json index 6c4b127..bb776f5 100644 --- a/client/build/asset-manifest.json +++ b/client/build/asset-manifest.json @@ -1,15 +1,15 @@ { "files": { - "main.css": "/static/css/main.4668db54.css", - "main.js": "/static/js/main.f791418f.js", + "main.css": "/static/css/main.b251a0fb.css", + "main.js": "/static/js/main.b7e3c916.js", "static/js/453.d456ce70.chunk.js": "/static/js/453.d456ce70.chunk.js", "index.html": "/index.html", - "main.4668db54.css.map": "/static/css/main.4668db54.css.map", - "main.f791418f.js.map": "/static/js/main.f791418f.js.map", + "main.b251a0fb.css.map": "/static/css/main.b251a0fb.css.map", + "main.b7e3c916.js.map": "/static/js/main.b7e3c916.js.map", "453.d456ce70.chunk.js.map": "/static/js/453.d456ce70.chunk.js.map" }, "entrypoints": [ - "static/css/main.4668db54.css", - "static/js/main.f791418f.js" + "static/css/main.b251a0fb.css", + "static/js/main.b7e3c916.js" ] } \ No newline at end of file diff --git a/client/build/index.html b/client/build/index.html index 3ec6ec8..88c8e95 100644 --- a/client/build/index.html +++ b/client/build/index.html @@ -1 +1 @@ -