gemini-computer-use 0.2.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +733 -16
- package/bin/cli.js +112 -17
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/agent.cpython-313.pyc +0 -0
- package/core/__pycache__/code_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/mcp_tools.cpython-313.pyc +0 -0
- package/core/__pycache__/schemas.cpython-313.pyc +0 -0
- package/core/__pycache__/server.cpython-313.pyc +0 -0
- package/core/__pycache__/vitals.cpython-313.pyc +0 -0
- package/core/__pycache__/web_share.cpython-313.pyc +0 -0
- package/core/agent.py +503 -18
- package/core/agent_harness.py +9 -2
- package/core/code_cache.py +15 -3
- package/core/device.py +2400 -0
- package/core/domain.py +303 -0
- package/core/mcp_tools.py +1840 -112
- package/core/policies.py +35 -5
- package/core/server.py +78 -32
- package/core/termux.py +916 -0
- package/core/updater.py +1713 -0
- package/core/version.py +31 -0
- package/core/vitals.py +66 -2
- package/core/web_share.py +814 -0
- package/gateway.py +1469 -98
- package/install-gui.cmd +40 -0
- package/install-gui.ps1 +1882 -0
- package/install-gui.strings.json +211 -0
- package/install.ps1 +960 -92
- package/install.sh +1214 -81
- package/ops/doctor.ps1 +735 -0
- package/ops/mesh-domain.sh +86 -0
- package/ops/nginx/README.md +77 -0
- package/ops/nginx/antigravity-mesh-mcp.conf +172 -0
- package/ops/nginx/mesh-domain.vhost.template +58 -0
- package/ops/nginx/render-domain.sh +123 -0
- package/ops/update.ps1 +373 -0
- package/ops/windows/README.md +298 -0
- package/ops/windows/agent-watchdog.ps1 +575 -0
- package/ops/windows/run-hidden.vbs +64 -0
- package/package.json +6 -2
- package/skills/orchestrator.md +12 -0
package/ops/doctor.ps1
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# Antigravity Mesh - Windows node doctor (read-only diagnostic)
|
|
3
|
+
#
|
|
4
|
+
# Answers "why is this node offline?" without changing anything on the machine:
|
|
5
|
+
# it only reads files, probes the pinned interpreter, asks the task scheduler and
|
|
6
|
+
# calls the gateway's public /health endpoint.
|
|
7
|
+
#
|
|
8
|
+
# It exists because of one real incident: after a Windows Update reboot the
|
|
9
|
+
# autostart silently did nothing. The interpreter pinned in the Startup .vbs was
|
|
10
|
+
# the Microsoft Store App Execution Alias <...>\WindowsApps\python.exe, whose only
|
|
11
|
+
# trace was a truncated "Python " line appended to agent.log WITHOUT a trailing
|
|
12
|
+
# newline - a torn line. The node stayed offline for ~29 hours. Every signal that
|
|
13
|
+
# would have made that obvious in five seconds is reported below, and the torn
|
|
14
|
+
# line is flagged explicitly (torn-line: true).
|
|
15
|
+
#
|
|
16
|
+
# WHAT IT REPORTS
|
|
17
|
+
# * the interpreter taken from the Startup .vbs, its --version, an
|
|
18
|
+
# "import websockets" probe, and whether it is the Store alias;
|
|
19
|
+
# * the .vbs path and its content, with the token masked as ****;
|
|
20
|
+
# * the Scheduled Task "AntigravityMeshWatchdog" (state, action, last run,
|
|
21
|
+
# last result) whenever the ScheduledTasks cmdlets are available;
|
|
22
|
+
# * agent.env with the token masked;
|
|
23
|
+
# * the heartbeat JSON and its age in seconds;
|
|
24
|
+
# * the last -TailLines lines of agent.log, plus torn-line: true when the log
|
|
25
|
+
# does not end with a newline;
|
|
26
|
+
# * the GET https://<gateway>/health?user=<node> JSON;
|
|
27
|
+
# * whether a VERIFIED agent process is running: a python.exe/pythonw.exe/py.exe
|
|
28
|
+
# running "-m core.agent", or the cmd.exe launcher wrapper that also carries
|
|
29
|
+
# the agent.log redirect. A process that merely mentions core.agent (a
|
|
30
|
+
# PowerShell session, a grep) is never reported as the agent;
|
|
31
|
+
# * the verdict lines interpreter_ok, autostart_ok, heartbeat_fresh,
|
|
32
|
+
# node_online.
|
|
33
|
+
#
|
|
34
|
+
# EXIT CODES
|
|
35
|
+
# 0 healthy: interpreter_ok, autostart_ok and node_online are all true
|
|
36
|
+
# 2 anything else (the verdict lines say which part failed)
|
|
37
|
+
#
|
|
38
|
+
# This file is deliberately ASCII-only: Windows PowerShell 5.1 reads a BOM-less
|
|
39
|
+
# script with the ANSI code page, so non-ASCII text here is a portability bug.
|
|
40
|
+
#
|
|
41
|
+
# Usage:
|
|
42
|
+
# .\doctor.ps1
|
|
43
|
+
# .\doctor.ps1 -Json
|
|
44
|
+
# .\doctor.ps1 -ConfigDir "$env:USERPROFILE\.config\antigravity-mesh" -TailLines 50
|
|
45
|
+
# ==============================================================================
|
|
46
|
+
[CmdletBinding()]
|
|
47
|
+
param(
|
|
48
|
+
[string]$ConfigDir = '',
|
|
49
|
+
[switch]$Json,
|
|
50
|
+
[int]$TailLines = 20
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
$ErrorActionPreference = 'Continue'
|
|
54
|
+
|
|
55
|
+
$AsJson = [bool]$Json
|
|
56
|
+
$TaskName = 'AntigravityMeshWatchdog'
|
|
57
|
+
# The watchdog treats a heartbeat as fresh within its -GraceSeconds (60 s by
|
|
58
|
+
# default) and runs every 5 minutes. The doctor has no -GraceSeconds parameter, so
|
|
59
|
+
# it is deliberately more generous: a heartbeat younger than this proves the agent
|
|
60
|
+
# is alive right now, and the exact age is always reported next to it.
|
|
61
|
+
$HeartbeatFreshSeconds = 180
|
|
62
|
+
$ProbeTimeoutMs = 20000
|
|
63
|
+
|
|
64
|
+
function Get-TextFile {
|
|
65
|
+
param([string]$Path)
|
|
66
|
+
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
|
67
|
+
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
|
68
|
+
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
|
69
|
+
}
|
|
70
|
+
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
|
71
|
+
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
|
72
|
+
}
|
|
73
|
+
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
|
74
|
+
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
|
75
|
+
}
|
|
76
|
+
$hasNul = $false
|
|
77
|
+
$limit = [Math]::Min($bytes.Length, 512)
|
|
78
|
+
for ($i = 0; $i -lt $limit; $i++) {
|
|
79
|
+
if ($bytes[$i] -eq 0) { $hasNul = $true; break }
|
|
80
|
+
}
|
|
81
|
+
if ($hasNul) { return [System.Text.Encoding]::Unicode.GetString($bytes) }
|
|
82
|
+
return [System.Text.Encoding]::UTF8.GetString($bytes)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Get-EnvMap {
|
|
86
|
+
param([string]$Path)
|
|
87
|
+
$values = @{}
|
|
88
|
+
try { $lines = @(Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction Stop) } catch { return $values }
|
|
89
|
+
foreach ($line in $lines) {
|
|
90
|
+
if ($null -eq $line) { continue }
|
|
91
|
+
$text = ([string]$line).Trim()
|
|
92
|
+
if ($text.Length -eq 0) { continue }
|
|
93
|
+
if ($text.StartsWith('#')) { continue }
|
|
94
|
+
$index = $text.IndexOf('=')
|
|
95
|
+
if ($index -lt 1) { continue }
|
|
96
|
+
$values[$text.Substring(0, $index).Trim()] = $text.Substring($index + 1).Trim().Trim('"').Trim("'")
|
|
97
|
+
}
|
|
98
|
+
return $values
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Nothing that a human can read leaves this script without passing through here:
|
|
102
|
+
# the literal secret (when long enough to be unambiguous) and both shipped shapes
|
|
103
|
+
# of the token line are replaced with ****.
|
|
104
|
+
function Hide-Secret {
|
|
105
|
+
param([string]$Text, [string]$Secret)
|
|
106
|
+
if ([string]::IsNullOrEmpty($Text)) { return $Text }
|
|
107
|
+
$result = $Text
|
|
108
|
+
if ($Secret -and $Secret.Length -ge 4) { $result = $result.Replace($Secret, '****') }
|
|
109
|
+
$result = [regex]::Replace($result, '(?im)^(\s*MESH_TOKEN\s*=\s*).*$', '${1}****')
|
|
110
|
+
$result = [regex]::Replace($result, '(?im)(MESH_TOKEN"\)\s*=\s*").*?(")', '${1}****${2}')
|
|
111
|
+
return $result
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function Get-LauncherInfo {
|
|
115
|
+
param([string]$VbsPath)
|
|
116
|
+
$info = @{ Present = $false; Text = ''; Interpreter = ''; PayloadDir = ''; Error = '' }
|
|
117
|
+
if (-not (Test-Path -LiteralPath $VbsPath -PathType Leaf)) { $info.Error = 'not found'; return $info }
|
|
118
|
+
$info.Present = $true
|
|
119
|
+
try {
|
|
120
|
+
$info.Text = Get-TextFile $VbsPath
|
|
121
|
+
} catch {
|
|
122
|
+
$info.Error = $_.Exception.Message
|
|
123
|
+
return $info
|
|
124
|
+
}
|
|
125
|
+
# The pattern stops at the line boundary ([^\r\n]*, no $ anchor: a .vbs written
|
|
126
|
+
# by Out-File has CRLF endings and .NET's multiline $ refuses to match before
|
|
127
|
+
# the \r of a CRLF pair).
|
|
128
|
+
$run = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?cmd[ \t]+/c[ \t]+"+(?<exe>[^"]+?)"+[ \t]+(?:-u[ \t]+)?-m[ \t]+core\.agent[^\r\n]*')
|
|
129
|
+
if (-not $run.Success) {
|
|
130
|
+
$run = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?(?<exe>[A-Za-z]:\\[^"]*?\.(?:exe|cmd|bat))')
|
|
131
|
+
}
|
|
132
|
+
if (-not $run.Success) {
|
|
133
|
+
$run = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?cmd[ \t]+/c[ \t]+"+(?<exe>[^"]+?)"+')
|
|
134
|
+
}
|
|
135
|
+
if ($run.Success) { $info.Interpreter = $run.Groups['exe'].Value.Trim() }
|
|
136
|
+
$dir = [regex]::Match($info.Text, '(?im)^[^\r\n]*WshShell\.CurrentDirectory[ \t]*=[ \t]*"(?<dir>[^"]+)"')
|
|
137
|
+
if ($dir.Success) { $info.PayloadDir = $dir.Groups['dir'].Value.Trim() }
|
|
138
|
+
return $info
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ConvertTo-UtcDateTime {
|
|
142
|
+
param($Value)
|
|
143
|
+
if ($null -eq $Value) { return $null }
|
|
144
|
+
if ($Value -is [datetime]) {
|
|
145
|
+
$stamp = [datetime]$Value
|
|
146
|
+
if ($stamp.Kind -eq [System.DateTimeKind]::Utc) { return $stamp }
|
|
147
|
+
if ($stamp.Kind -eq [System.DateTimeKind]::Local) { return $stamp.ToUniversalTime() }
|
|
148
|
+
return [System.DateTime]::SpecifyKind($stamp, [System.DateTimeKind]::Utc)
|
|
149
|
+
}
|
|
150
|
+
$text = ([string]$Value).Trim().Trim('"')
|
|
151
|
+
if ($text.Length -eq 0) { return $null }
|
|
152
|
+
$epoch = New-Object System.DateTime -ArgumentList @(1970, 1, 1, 0, 0, 0, [System.DateTimeKind]::Utc)
|
|
153
|
+
$number = 0.0
|
|
154
|
+
$isNumber = $false
|
|
155
|
+
if ($Value -is [double] -or $Value -is [single] -or $Value -is [decimal] -or $Value -is [int] -or $Value -is [long]) {
|
|
156
|
+
$number = [double]$Value
|
|
157
|
+
$isNumber = $true
|
|
158
|
+
} elseif ([double]::TryParse($text, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$number)) {
|
|
159
|
+
$isNumber = $true
|
|
160
|
+
}
|
|
161
|
+
# core/agent.py writes "ts" as a Unix timestamp in seconds, often fractional.
|
|
162
|
+
# Numbers are read with the invariant culture on purpose: on a comma-decimal
|
|
163
|
+
# locale "1767225600.25" would otherwise fail to parse and a live node would be
|
|
164
|
+
# declared dead.
|
|
165
|
+
if ($isNumber) {
|
|
166
|
+
# Unix seconds (10 digits) or Unix milliseconds (13 digits).
|
|
167
|
+
if ($number -gt 100000000000.0) { return $epoch.AddMilliseconds($number) }
|
|
168
|
+
if ($number -gt 100000000.0) { return $epoch.AddSeconds($number) }
|
|
169
|
+
return $null
|
|
170
|
+
}
|
|
171
|
+
$styles = [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal
|
|
172
|
+
$parsed = [datetime]::MinValue
|
|
173
|
+
if ([datetime]::TryParse($text, [System.Globalization.CultureInfo]::InvariantCulture, $styles, [ref]$parsed)) {
|
|
174
|
+
return [System.DateTime]::SpecifyKind($parsed, [System.DateTimeKind]::Utc)
|
|
175
|
+
}
|
|
176
|
+
return $null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function Get-HeartbeatInfo {
|
|
180
|
+
param([string]$Path, [int]$FreshSeconds)
|
|
181
|
+
$info = @{ Present = $false; Parsed = $false; Json = ''; AgeSeconds = $null; Fresh = $false; Detail = 'missing' }
|
|
182
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $info }
|
|
183
|
+
$info.Present = $true
|
|
184
|
+
try {
|
|
185
|
+
$raw = [System.IO.File]::ReadAllText($Path)
|
|
186
|
+
} catch {
|
|
187
|
+
$info.Detail = 'unreadable: ' + $_.Exception.Message
|
|
188
|
+
return $info
|
|
189
|
+
}
|
|
190
|
+
$info.Json = $raw.Trim()
|
|
191
|
+
try {
|
|
192
|
+
$parsed = $raw | ConvertFrom-Json
|
|
193
|
+
} catch {
|
|
194
|
+
$info.Detail = 'not valid JSON'
|
|
195
|
+
return $info
|
|
196
|
+
}
|
|
197
|
+
$info.Parsed = $true
|
|
198
|
+
$stampRaw = $null
|
|
199
|
+
foreach ($name in @('ts', 'timestamp', 'time', 'last_seen', 'checked_at')) {
|
|
200
|
+
$prop = $parsed.PSObject.Properties[$name]
|
|
201
|
+
if ($prop -and $null -ne $prop.Value -and ([string]$prop.Value).Trim().Length -gt 0) {
|
|
202
|
+
$stampRaw = $prop.Value
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if ($null -eq $stampRaw) { $info.Detail = 'JSON has no ts field'; return $info }
|
|
207
|
+
$stamp = ConvertTo-UtcDateTime $stampRaw
|
|
208
|
+
if ($null -eq $stamp) { $info.Detail = 'ts is not a timestamp: ' + ([string]$stampRaw); return $info }
|
|
209
|
+
$age = ((Get-Date).ToUniversalTime() - $stamp.ToUniversalTime()).TotalSeconds
|
|
210
|
+
$info.AgeSeconds = [Math]::Round($age, 1)
|
|
211
|
+
if ($age -le $FreshSeconds) {
|
|
212
|
+
$info.Fresh = $true
|
|
213
|
+
$info.Detail = 'fresh (' + $info.AgeSeconds + 's old, fresh within ' + $FreshSeconds + 's)'
|
|
214
|
+
} else {
|
|
215
|
+
$info.Detail = 'stale (' + $info.AgeSeconds + 's old, fresh within ' + $FreshSeconds + 's)'
|
|
216
|
+
}
|
|
217
|
+
return $info
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function Invoke-Captured {
|
|
221
|
+
# Runs a program and returns its output, bounded by a timeout: the Store alias
|
|
222
|
+
# can hang instead of failing, and a diagnostic must not hang with it.
|
|
223
|
+
param([string]$FilePath, [string]$ArgumentLine, [string]$WorkingDirectory = '', [int]$TimeoutMs = 20000)
|
|
224
|
+
$result = @{ Ran = $false; ExitCode = $null; TimedOut = $false; Output = ''; Error = '' }
|
|
225
|
+
if ([string]::IsNullOrWhiteSpace($FilePath)) { $result.Error = 'no program to run'; return $result }
|
|
226
|
+
if ([string]::IsNullOrWhiteSpace($WorkingDirectory) -or -not (Test-Path -LiteralPath $WorkingDirectory -PathType Container)) {
|
|
227
|
+
$WorkingDirectory = ''
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
|
231
|
+
$startInfo.FileName = $FilePath
|
|
232
|
+
$startInfo.Arguments = $ArgumentLine
|
|
233
|
+
$startInfo.UseShellExecute = $false
|
|
234
|
+
$startInfo.RedirectStandardOutput = $true
|
|
235
|
+
$startInfo.RedirectStandardError = $true
|
|
236
|
+
$startInfo.CreateNoWindow = $true
|
|
237
|
+
if ($WorkingDirectory) { $startInfo.WorkingDirectory = $WorkingDirectory }
|
|
238
|
+
$process = [System.Diagnostics.Process]::Start($startInfo)
|
|
239
|
+
$result.Ran = $true
|
|
240
|
+
$exited = $process.WaitForExit($TimeoutMs)
|
|
241
|
+
if (-not $exited) {
|
|
242
|
+
$result.TimedOut = $true
|
|
243
|
+
try { $process.Kill() } catch { }
|
|
244
|
+
$null = $process.WaitForExit(3000)
|
|
245
|
+
}
|
|
246
|
+
$stdout = $process.StandardOutput.ReadToEnd()
|
|
247
|
+
$stderr = $process.StandardError.ReadToEnd()
|
|
248
|
+
if ($exited) { $result.ExitCode = [int]$process.ExitCode }
|
|
249
|
+
$result.Output = (($stdout + ' ' + $stderr) -replace '\s+', ' ').Trim()
|
|
250
|
+
} catch {
|
|
251
|
+
$result.Error = $_.Exception.Message
|
|
252
|
+
}
|
|
253
|
+
return $result
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function Get-ScheduledTaskState {
|
|
257
|
+
param([string]$Name)
|
|
258
|
+
$state = @{
|
|
259
|
+
CmdletsAvailable = $false; Present = $false; Name = $Name; State = ''; Actions = @()
|
|
260
|
+
LastRunTime = ''; LastResult = $null; LastResultMeaning = ''; NextRunTime = ''
|
|
261
|
+
MissedRuns = $null; Detail = ''
|
|
262
|
+
}
|
|
263
|
+
if (-not (Get-Command -Name 'Get-ScheduledTask' -ErrorAction SilentlyContinue)) {
|
|
264
|
+
$state.Detail = 'the ScheduledTasks cmdlets are not available on this system'
|
|
265
|
+
return $state
|
|
266
|
+
}
|
|
267
|
+
$state.CmdletsAvailable = $true
|
|
268
|
+
try {
|
|
269
|
+
$task = Get-ScheduledTask -TaskName $Name -ErrorAction Stop
|
|
270
|
+
} catch {
|
|
271
|
+
$state.Detail = 'not registered'
|
|
272
|
+
return $state
|
|
273
|
+
}
|
|
274
|
+
$state.Present = $true
|
|
275
|
+
$state.State = [string]$task.State
|
|
276
|
+
$actions = @()
|
|
277
|
+
foreach ($action in @($task.Actions)) {
|
|
278
|
+
$text = ([string]$action.Execute).Trim()
|
|
279
|
+
if ($action.Arguments) { $text = ($text + ' ' + [string]$action.Arguments).Trim() }
|
|
280
|
+
if ($text.Length -gt 0) { $actions += $text }
|
|
281
|
+
}
|
|
282
|
+
$state.Actions = $actions
|
|
283
|
+
try {
|
|
284
|
+
$info = Get-ScheduledTaskInfo -TaskName $Name -ErrorAction Stop
|
|
285
|
+
if ($info) {
|
|
286
|
+
if ($info.LastRunTime) { $state.LastRunTime = ([datetime]$info.LastRunTime).ToString('o') }
|
|
287
|
+
if ($null -ne $info.LastTaskResult) { $state.LastResult = [int64]$info.LastTaskResult }
|
|
288
|
+
if ($info.NextRunTime) { $state.NextRunTime = ([datetime]$info.NextRunTime).ToString('o') }
|
|
289
|
+
if ($null -ne $info.NumberOfMissedRuns) { $state.MissedRuns = [int64]$info.NumberOfMissedRuns }
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
$state.Detail = 'registered, but its run information is unreadable: ' + $_.Exception.Message
|
|
293
|
+
}
|
|
294
|
+
$meanings = @{
|
|
295
|
+
0 = 'ok'
|
|
296
|
+
2 = 'the watchdog refused the configuration (agent.env, the .vbs or the interpreter)'
|
|
297
|
+
3 = 'the watchdog tried to recover the node and it was still offline'
|
|
298
|
+
267009 = 'the task is currently running'
|
|
299
|
+
267011 = 'the task has not run yet'
|
|
300
|
+
2147942401 = 'access denied (the task may require elevation)'
|
|
301
|
+
}
|
|
302
|
+
if ($null -ne $state.LastResult) {
|
|
303
|
+
if ($meanings.ContainsKey([int]$state.LastResult)) { $state.LastResultMeaning = $meanings[[int]$state.LastResult] }
|
|
304
|
+
else { $state.LastResultMeaning = 'unrecognised result code' }
|
|
305
|
+
}
|
|
306
|
+
return $state
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function Test-AgentProcess {
|
|
310
|
+
# The command line ALONE is not evidence. A PowerShell session that merely
|
|
311
|
+
# mentions "core.agent" (a grep, a script that prints the module name) matches
|
|
312
|
+
# the bare substring, so a dead node would look alive and would never be
|
|
313
|
+
# restarted. The process must be a Python interpreter running the module, or
|
|
314
|
+
# the cmd.exe wrapper the logon launcher uses - which also carries the
|
|
315
|
+
# agent.log append redirect. PowerShell, wscript and everything else never
|
|
316
|
+
# qualifies, whatever its command line says.
|
|
317
|
+
param([string]$Name, [string]$CommandLine)
|
|
318
|
+
if ([string]::IsNullOrWhiteSpace($CommandLine)) { return $false }
|
|
319
|
+
$processName = ''
|
|
320
|
+
if ($Name) { $processName = ([string]$Name).Trim().ToLowerInvariant() }
|
|
321
|
+
foreach ($shell in @('powershell', 'pwsh', 'wscript', 'cscript')) {
|
|
322
|
+
if ($processName.StartsWith($shell)) { return $false }
|
|
323
|
+
}
|
|
324
|
+
# "-m core.agent" has to be a real argument, so it must open the command line
|
|
325
|
+
# or follow a space or a quote.
|
|
326
|
+
if ($CommandLine -notmatch '(?i)(^|[\s"])-m[\s"]+core\.agent\b') { return $false }
|
|
327
|
+
if ($processName -eq 'py.exe') { return $true }
|
|
328
|
+
if ($processName.StartsWith('python')) { return $true }
|
|
329
|
+
if ($processName -eq 'cmd.exe' -or $processName -eq 'cmd') {
|
|
330
|
+
# Launcher wrapper: cmd /c ""<python>" -u -m core.agent >> "<agent.log>" 2>&1"
|
|
331
|
+
return [bool]($CommandLine -match '(?i)agent\.log')
|
|
332
|
+
}
|
|
333
|
+
return $false
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function Get-AgentProcesses {
|
|
337
|
+
# Read-only, and deliberately the SAME rule as the watchdog's last liveness
|
|
338
|
+
# check, so a reader can tell "the agent is dead" from "the gateway cannot see
|
|
339
|
+
# it" without a different answer from the two scripts.
|
|
340
|
+
$found = @()
|
|
341
|
+
$procs = @()
|
|
342
|
+
try {
|
|
343
|
+
$procs = @(Get-CimInstance -ClassName Win32_Process -ErrorAction Stop)
|
|
344
|
+
} catch {
|
|
345
|
+
try { $procs = @(Get-WmiObject -Class Win32_Process -ErrorAction Stop) } catch { $procs = @() }
|
|
346
|
+
}
|
|
347
|
+
foreach ($proc in $procs) {
|
|
348
|
+
$commandLine = ''
|
|
349
|
+
$prop = $proc.PSObject.Properties['CommandLine']
|
|
350
|
+
if ($prop -and $prop.Value) { $commandLine = [string]$prop.Value }
|
|
351
|
+
if (-not (Test-AgentProcess -Name ([string]$proc.Name) -CommandLine $commandLine)) { continue }
|
|
352
|
+
$found += @{ Pid = [int]$proc.ProcessId; Name = [string]$proc.Name; CommandLine = $commandLine }
|
|
353
|
+
}
|
|
354
|
+
return $found
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function Get-LastByte {
|
|
358
|
+
param([string]$Path)
|
|
359
|
+
try {
|
|
360
|
+
$stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
361
|
+
try {
|
|
362
|
+
if ($stream.Length -eq 0) { return $null }
|
|
363
|
+
$null = $stream.Seek(-1, [System.IO.SeekOrigin]::End)
|
|
364
|
+
return $stream.ReadByte()
|
|
365
|
+
} finally { $stream.Dispose() }
|
|
366
|
+
} catch {
|
|
367
|
+
return $null
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function Get-LogInfo {
|
|
372
|
+
param([string]$Path, [int]$Lines)
|
|
373
|
+
$info = @{ Present = $false; Bytes = 0; LastWriteTime = ''; Tail = @(); TornLine = $false; LastByte = $null; Error = '' }
|
|
374
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $info }
|
|
375
|
+
$info.Present = $true
|
|
376
|
+
try {
|
|
377
|
+
$item = Get-Item -LiteralPath $Path -ErrorAction Stop
|
|
378
|
+
$info.Bytes = [int64]$item.Length
|
|
379
|
+
$info.LastWriteTime = ([datetime]$item.LastWriteTimeUtc).ToString('o')
|
|
380
|
+
} catch {
|
|
381
|
+
$info.Error = $_.Exception.Message
|
|
382
|
+
}
|
|
383
|
+
if ($Lines -gt 0) {
|
|
384
|
+
try {
|
|
385
|
+
$info.Tail = @(Get-Content -LiteralPath $Path -Tail $Lines -Encoding UTF8 -ErrorAction Stop)
|
|
386
|
+
} catch {
|
|
387
|
+
$info.Error = $_.Exception.Message
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
# A log whose last byte is not LF ends in a torn line - the truncated
|
|
391
|
+
# "Python " fragment was the only trace the incident left behind.
|
|
392
|
+
$info.LastByte = Get-LastByte $Path
|
|
393
|
+
if ($null -ne $info.LastByte -and $info.LastByte -ne 0x0A) { $info.TornLine = $true }
|
|
394
|
+
return $info
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function Get-GatewayInfo {
|
|
398
|
+
param([string]$Gateway, [string]$NodeUser, [int]$TimeoutSec = 15)
|
|
399
|
+
$info = @{ Url = ''; Checked = $false; Online = $false; Json = ''; Detail = 'not checked' }
|
|
400
|
+
if ([string]::IsNullOrWhiteSpace($Gateway) -or [string]::IsNullOrWhiteSpace($NodeUser)) {
|
|
401
|
+
$info.Detail = 'gateway or node name is missing from agent.env'
|
|
402
|
+
return $info
|
|
403
|
+
}
|
|
404
|
+
$hostName = ($Gateway.Trim() -replace '^[A-Za-z][A-Za-z0-9+.-]*://', '').Trim().TrimEnd('/')
|
|
405
|
+
if ($hostName.Length -eq 0) {
|
|
406
|
+
$info.Detail = 'the gateway value is empty after normalisation'
|
|
407
|
+
return $info
|
|
408
|
+
}
|
|
409
|
+
$info.Url = 'https://' + $hostName + '/health?user=' + [uri]::EscapeDataString($NodeUser)
|
|
410
|
+
$info.Checked = $true
|
|
411
|
+
try {
|
|
412
|
+
$response = Invoke-RestMethod -Uri $info.Url -Method Get -TimeoutSec $TimeoutSec -ErrorAction Stop
|
|
413
|
+
} catch {
|
|
414
|
+
$info.Detail = 'unreachable: ' + $_.Exception.Message
|
|
415
|
+
return $info
|
|
416
|
+
}
|
|
417
|
+
try { $info.Json = ($response | ConvertTo-Json -Compress -Depth 5) } catch { $info.Json = '' }
|
|
418
|
+
$prop = $response.PSObject.Properties['node_online']
|
|
419
|
+
$online = $false
|
|
420
|
+
if ($prop) {
|
|
421
|
+
if ($prop.Value -is [bool]) { $online = [bool]$prop.Value } else { $online = (([string]$prop.Value).Trim().ToLower() -eq 'true') }
|
|
422
|
+
}
|
|
423
|
+
$info.Online = $online
|
|
424
|
+
if ($online) { $info.Detail = 'node_online=true' } else { $info.Detail = 'node_online=false' }
|
|
425
|
+
return $info
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# ==============================================================================
|
|
429
|
+
# Collect (read-only)
|
|
430
|
+
# ==============================================================================
|
|
431
|
+
try {
|
|
432
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
433
|
+
} catch { }
|
|
434
|
+
|
|
435
|
+
if ([string]::IsNullOrWhiteSpace($ConfigDir)) {
|
|
436
|
+
$profileDir = $env:USERPROFILE
|
|
437
|
+
if ([string]::IsNullOrWhiteSpace($profileDir)) { $profileDir = [Environment]::GetFolderPath('UserProfile') }
|
|
438
|
+
if ([string]::IsNullOrWhiteSpace($profileDir)) { $profileDir = (Get-Location).Path }
|
|
439
|
+
$ConfigDir = Join-Path $profileDir '.config\antigravity-mesh'
|
|
440
|
+
}
|
|
441
|
+
$envFile = Join-Path $ConfigDir 'agent.env'
|
|
442
|
+
$settings = Get-EnvMap $envFile
|
|
443
|
+
$gateway = ''
|
|
444
|
+
$nodeUser = ''
|
|
445
|
+
$token = ''
|
|
446
|
+
if ($settings.ContainsKey('MESH_GATEWAY')) { $gateway = [string]$settings['MESH_GATEWAY'] }
|
|
447
|
+
if ($settings.ContainsKey('MESH_USER')) { $nodeUser = [string]$settings['MESH_USER'] }
|
|
448
|
+
if ($settings.ContainsKey('MESH_TOKEN')) { $token = [string]$settings['MESH_TOKEN'] }
|
|
449
|
+
|
|
450
|
+
$missingKeys = @()
|
|
451
|
+
foreach ($key in @('MESH_GATEWAY', 'MESH_USER', 'MESH_TOKEN')) {
|
|
452
|
+
if (-not $settings.ContainsKey($key) -or [string]::IsNullOrWhiteSpace($settings[$key])) { $missingKeys += $key }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
$envPresent = Test-Path -LiteralPath $envFile -PathType Leaf
|
|
456
|
+
$envRaw = ''
|
|
457
|
+
if ($envPresent) {
|
|
458
|
+
try { $envRaw = Get-TextFile $envFile } catch { $envRaw = 'unreadable: ' + $_.Exception.Message }
|
|
459
|
+
}
|
|
460
|
+
$envMasked = Hide-Secret -Text $envRaw -Secret $token
|
|
461
|
+
|
|
462
|
+
# --- launcher -----------------------------------------------------------------
|
|
463
|
+
$startupDir = [Environment]::GetFolderPath('Startup')
|
|
464
|
+
$vbsPath = ''
|
|
465
|
+
if ([string]::IsNullOrWhiteSpace($startupDir)) { $vbsPath = 'antigravity-agent.vbs (the Startup folder could not be resolved)' }
|
|
466
|
+
else { $vbsPath = Join-Path $startupDir 'antigravity-agent.vbs' }
|
|
467
|
+
$launcher = Get-LauncherInfo $vbsPath
|
|
468
|
+
$interpreter = $launcher.Interpreter
|
|
469
|
+
$storeAlias = [bool]($interpreter -and ($interpreter -match '(?i)\\WindowsApps\\'))
|
|
470
|
+
$interpreterExists = [bool]($interpreter -and (Test-Path -LiteralPath $interpreter -PathType Leaf))
|
|
471
|
+
|
|
472
|
+
$payloadDir = $launcher.PayloadDir
|
|
473
|
+
$payloadProblem = ''
|
|
474
|
+
if ([string]::IsNullOrWhiteSpace($payloadDir)) {
|
|
475
|
+
$payloadProblem = 'WshShell.CurrentDirectory is missing from the launcher'
|
|
476
|
+
} elseif (-not (Test-Path -LiteralPath $payloadDir -PathType Container)) {
|
|
477
|
+
$payloadProblem = 'WshShell.CurrentDirectory is not a directory: ' + $payloadDir
|
|
478
|
+
}
|
|
479
|
+
$payloadOk = [bool]($payloadProblem -eq '')
|
|
480
|
+
$coreModule = ''
|
|
481
|
+
if ($payloadOk) {
|
|
482
|
+
$coreModule = Join-Path $payloadDir 'core\agent.py'
|
|
483
|
+
}
|
|
484
|
+
$coreModuleOk = [bool]($coreModule -and (Test-Path -LiteralPath $coreModule -PathType Leaf))
|
|
485
|
+
|
|
486
|
+
# --- interpreter probes -------------------------------------------------------
|
|
487
|
+
$versionProbe = Invoke-Captured -FilePath $interpreter -ArgumentLine '--version' -WorkingDirectory $payloadDir -TimeoutMs $ProbeTimeoutMs
|
|
488
|
+
$websocketsProbe = Invoke-Captured -FilePath $interpreter -ArgumentLine '-c "import websockets"' -WorkingDirectory $payloadDir -TimeoutMs $ProbeTimeoutMs
|
|
489
|
+
$versionOk = [bool]($interpreterExists -and $versionProbe.Ran -and (-not $versionProbe.TimedOut) -and ($versionProbe.ExitCode -eq 0) -and ($versionProbe.Output -match '(?i)^Python\s+\d'))
|
|
490
|
+
$websocketsOk = [bool]($interpreterExists -and $websocketsProbe.Ran -and (-not $websocketsProbe.TimedOut) -and ($websocketsProbe.ExitCode -eq 0))
|
|
491
|
+
|
|
492
|
+
$interpreterOk = [bool]($interpreter -and (-not $storeAlias) -and $interpreterExists -and $versionOk)
|
|
493
|
+
$autostartOk = [bool]($launcher.Present -and $interpreter -and (-not $storeAlias) -and $interpreterExists -and $payloadOk)
|
|
494
|
+
|
|
495
|
+
# --- heartbeat / log / task / gateway ----------------------------------------
|
|
496
|
+
$heartbeatPath = Join-Path $ConfigDir 'agent.heartbeat'
|
|
497
|
+
$heartbeat = Get-HeartbeatInfo -Path $heartbeatPath -FreshSeconds $HeartbeatFreshSeconds
|
|
498
|
+
$agentLogPath = Join-Path $ConfigDir 'agent.log'
|
|
499
|
+
$logLines = [Math]::Max(0, $TailLines)
|
|
500
|
+
$log = Get-LogInfo -Path $agentLogPath -Lines $logLines
|
|
501
|
+
$task = Get-ScheduledTaskState -Name $TaskName
|
|
502
|
+
$processes = @(Get-AgentProcesses)
|
|
503
|
+
$health = Get-GatewayInfo -Gateway $gateway -NodeUser $nodeUser
|
|
504
|
+
|
|
505
|
+
$healthy = [bool]($interpreterOk -and $autostartOk -and $health.Online)
|
|
506
|
+
|
|
507
|
+
$problems = @()
|
|
508
|
+
if (-not $interpreterOk) {
|
|
509
|
+
if (-not $interpreter) { $problems += 'no interpreter could be read from the launcher' }
|
|
510
|
+
elseif ($storeAlias) { $problems += 'the pinned interpreter is the Microsoft Store alias (it never runs Python)' }
|
|
511
|
+
elseif (-not $interpreterExists) { $problems += 'the pinned interpreter does not exist' }
|
|
512
|
+
elseif (-not $versionOk) { $problems += ('the pinned interpreter does not answer --version' + $(if ($versionProbe.TimedOut) { ' (it timed out)' } else { '' })) }
|
|
513
|
+
}
|
|
514
|
+
if (-not $autostartOk) {
|
|
515
|
+
if (-not $launcher.Present) { $problems += 'the Startup launcher is missing: ' + $vbsPath }
|
|
516
|
+
if (-not $payloadOk) { $problems += $payloadProblem }
|
|
517
|
+
}
|
|
518
|
+
if ($autostartOk -and -not $coreModuleOk) { $problems += 'core\agent.py is not in the payload directory: ' + $coreModule }
|
|
519
|
+
if ($log.TornLine) { $problems += 'agent.log ends in a torn line (a truncated write, exactly like the post-reboot "Python " failure)' }
|
|
520
|
+
if (-not $health.Online) { $problems += 'the gateway does not report this node as online: ' + $health.Detail }
|
|
521
|
+
if ($processes.Count -eq 0) { $problems += 'no agent process found: nothing is running python -m core.agent (the process list may also be unreadable from this session)' }
|
|
522
|
+
|
|
523
|
+
# ==============================================================================
|
|
524
|
+
# Report
|
|
525
|
+
# ==============================================================================
|
|
526
|
+
$report = [ordered]@{
|
|
527
|
+
generated_at = ((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss') + 'Z')
|
|
528
|
+
config_dir = $ConfigDir
|
|
529
|
+
launcher = [ordered]@{
|
|
530
|
+
path = $vbsPath
|
|
531
|
+
present = $launcher.Present
|
|
532
|
+
read_error = $launcher.Error
|
|
533
|
+
interpreter = $interpreter
|
|
534
|
+
payload_dir = $payloadDir
|
|
535
|
+
payload_ok = $payloadOk
|
|
536
|
+
payload_problem = $payloadProblem
|
|
537
|
+
core_module = $coreModule
|
|
538
|
+
core_module_present = $coreModuleOk
|
|
539
|
+
content = (Hide-Secret -Text $launcher.Text -Secret $token)
|
|
540
|
+
}
|
|
541
|
+
interpreter = [ordered]@{
|
|
542
|
+
path = $interpreter
|
|
543
|
+
exists = $interpreterExists
|
|
544
|
+
store_alias = $storeAlias
|
|
545
|
+
version = $versionProbe.Output
|
|
546
|
+
version_exit_code = $versionProbe.ExitCode
|
|
547
|
+
version_timed_out = $versionProbe.TimedOut
|
|
548
|
+
version_ok = $versionOk
|
|
549
|
+
websockets_ok = $websocketsOk
|
|
550
|
+
websockets_output = $websocketsProbe.Output
|
|
551
|
+
probe_error = ($versionProbe.Error + ' ' + $websocketsProbe.Error).Trim()
|
|
552
|
+
probe_working_directory = $payloadDir
|
|
553
|
+
}
|
|
554
|
+
scheduled_task = [ordered]@{
|
|
555
|
+
name = $task.Name
|
|
556
|
+
cmdlets_available = $task.CmdletsAvailable
|
|
557
|
+
present = $task.Present
|
|
558
|
+
state = $task.State
|
|
559
|
+
actions = $task.Actions
|
|
560
|
+
last_run_time = $task.LastRunTime
|
|
561
|
+
last_result = $task.LastResult
|
|
562
|
+
last_result_meaning = $task.LastResultMeaning
|
|
563
|
+
next_run_time = $task.NextRunTime
|
|
564
|
+
missed_runs = $task.MissedRuns
|
|
565
|
+
detail = $task.Detail
|
|
566
|
+
}
|
|
567
|
+
agent_env = [ordered]@{
|
|
568
|
+
path = $envFile
|
|
569
|
+
present = $envPresent
|
|
570
|
+
gateway = $gateway
|
|
571
|
+
user = $nodeUser
|
|
572
|
+
token = '****'
|
|
573
|
+
missing_keys = $missingKeys
|
|
574
|
+
content = $envMasked
|
|
575
|
+
}
|
|
576
|
+
heartbeat = [ordered]@{
|
|
577
|
+
path = $heartbeatPath
|
|
578
|
+
present = $heartbeat.Present
|
|
579
|
+
parsed = $heartbeat.Parsed
|
|
580
|
+
json = $heartbeat.Json
|
|
581
|
+
age_seconds = $heartbeat.AgeSeconds
|
|
582
|
+
fresh = $heartbeat.Fresh
|
|
583
|
+
fresh_within_seconds = $HeartbeatFreshSeconds
|
|
584
|
+
detail = $heartbeat.Detail
|
|
585
|
+
}
|
|
586
|
+
agent_log = [ordered]@{
|
|
587
|
+
path = $agentLogPath
|
|
588
|
+
present = $log.Present
|
|
589
|
+
bytes = $log.Bytes
|
|
590
|
+
last_write_utc = $log.LastWriteTime
|
|
591
|
+
tail_lines = $logLines
|
|
592
|
+
lines = $log.Tail
|
|
593
|
+
torn_line = $log.TornLine
|
|
594
|
+
last_byte = $log.LastByte
|
|
595
|
+
read_error = $log.Error
|
|
596
|
+
}
|
|
597
|
+
agent_processes = @($processes | ForEach-Object { [ordered]@{ pid = $_.Pid; name = $_.Name; command_line = $_.CommandLine } })
|
|
598
|
+
gateway = [ordered]@{
|
|
599
|
+
url = $health.Url
|
|
600
|
+
checked = $health.Checked
|
|
601
|
+
node_online = $health.Online
|
|
602
|
+
json = $health.Json
|
|
603
|
+
detail = $health.Detail
|
|
604
|
+
}
|
|
605
|
+
problems = $problems
|
|
606
|
+
verdict = [ordered]@{
|
|
607
|
+
interpreter_ok = $interpreterOk
|
|
608
|
+
autostart_ok = $autostartOk
|
|
609
|
+
heartbeat_fresh = $heartbeat.Fresh
|
|
610
|
+
node_online = $health.Online
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if ($AsJson) {
|
|
615
|
+
# Exactly one JSON object, nothing else: the exit code carries the verdict, and
|
|
616
|
+
# ConvertTo-Json escapes any non-ASCII log text, so this stays 7-bit clean.
|
|
617
|
+
$report | ConvertTo-Json -Depth 8
|
|
618
|
+
} else {
|
|
619
|
+
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
|
620
|
+
|
|
621
|
+
$out = New-Object System.Collections.ArrayList
|
|
622
|
+
function Add-Out {
|
|
623
|
+
param([string]$Text)
|
|
624
|
+
[void]$out.Add($Text)
|
|
625
|
+
}
|
|
626
|
+
function Add-Field {
|
|
627
|
+
param([string]$Name, $Value)
|
|
628
|
+
# Booleans print as lowercase true/false so the verdict lines can be read by
|
|
629
|
+
# eye and by grep, exactly as documented.
|
|
630
|
+
if ($Value -is [bool]) {
|
|
631
|
+
$text = 'false'
|
|
632
|
+
if ($Value) { $text = 'true' }
|
|
633
|
+
} else {
|
|
634
|
+
$text = ''
|
|
635
|
+
if ($null -ne $Value) { $text = [string]$Value }
|
|
636
|
+
}
|
|
637
|
+
[void]$out.Add(('{0,-22} : {1}' -f $Name, $text))
|
|
638
|
+
}
|
|
639
|
+
function Add-Block {
|
|
640
|
+
param([string]$Name, [string[]]$Lines)
|
|
641
|
+
Add-Out ($Name + ':')
|
|
642
|
+
if (-not $Lines -or $Lines.Count -eq 0) { Add-Out ' (empty)'; return }
|
|
643
|
+
foreach ($line in $Lines) { Add-Out (' ' + $line) }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
Add-Out 'Antigravity Mesh doctor (read-only)'
|
|
647
|
+
Add-Out '================================='
|
|
648
|
+
Add-Field 'generated_at' $report.generated_at
|
|
649
|
+
Add-Field 'config_dir' $ConfigDir
|
|
650
|
+
Add-Out ''
|
|
651
|
+
Add-Out '[launcher - Windows Startup .vbs]'
|
|
652
|
+
Add-Field 'vbs_path' $vbsPath
|
|
653
|
+
Add-Field 'vbs_present' $launcher.Present
|
|
654
|
+
if ($launcher.Error) { Add-Field -Name 'vbs_read_error' -Value $launcher.Error }
|
|
655
|
+
Add-Field 'vbs_interpreter' $interpreter
|
|
656
|
+
Add-Field 'payload_dir' $payloadDir
|
|
657
|
+
Add-Field 'payload_ok' $payloadOk
|
|
658
|
+
if ($payloadProblem) { Add-Field -Name 'payload_problem' -Value $payloadProblem }
|
|
659
|
+
Add-Field 'core_module_present' $coreModuleOk
|
|
660
|
+
if ($report.launcher.content) { Add-Block 'vbs_content (token masked)' @($report.launcher.content -split "`r?`n") }
|
|
661
|
+
Add-Out ''
|
|
662
|
+
Add-Out '[interpreter]'
|
|
663
|
+
Add-Field 'interpreter_exists' $interpreterExists
|
|
664
|
+
Add-Field 'store_alias' $storeAlias
|
|
665
|
+
Add-Field -Name 'interpreter_version' -Value $versionProbe.Output
|
|
666
|
+
Add-Field 'version_exit_code' $versionProbe.ExitCode
|
|
667
|
+
Add-Field 'version_timed_out' $versionProbe.TimedOut
|
|
668
|
+
Add-Field 'websockets_ok' $websocketsOk
|
|
669
|
+
Add-Field -Name 'websockets_probe' -Value $websocketsProbe.Output
|
|
670
|
+
Add-Field 'probe_cwd' $payloadDir
|
|
671
|
+
Add-Out ''
|
|
672
|
+
Add-Out '[scheduled task]'
|
|
673
|
+
Add-Field 'task_name' $TaskName
|
|
674
|
+
Add-Field 'task_cmdlets_available' $task.CmdletsAvailable
|
|
675
|
+
Add-Field 'task_present' $task.Present
|
|
676
|
+
Add-Field 'task_state' $task.State
|
|
677
|
+
Add-Field 'task_last_run_time' $task.LastRunTime
|
|
678
|
+
Add-Field 'task_last_result' $task.LastResult
|
|
679
|
+
Add-Field 'task_last_result_meaning' $task.LastResultMeaning
|
|
680
|
+
Add-Field 'task_next_run_time' $task.NextRunTime
|
|
681
|
+
Add-Field 'task_missed_runs' $task.MissedRuns
|
|
682
|
+
if ($task.Detail) { Add-Field -Name 'task_detail' -Value $task.Detail }
|
|
683
|
+
Add-Block 'task_actions' @($task.Actions)
|
|
684
|
+
Add-Out ''
|
|
685
|
+
Add-Out '[agent.env]'
|
|
686
|
+
Add-Field 'agent_env_path' $envFile
|
|
687
|
+
Add-Field 'agent_env_present' $envPresent
|
|
688
|
+
Add-Field 'MESH_GATEWAY' $gateway
|
|
689
|
+
Add-Field 'MESH_USER' $nodeUser
|
|
690
|
+
Add-Field 'MESH_TOKEN' '****'
|
|
691
|
+
if ($missingKeys.Count -gt 0) { Add-Field 'missing_keys' ($missingKeys -join ', ') }
|
|
692
|
+
if ($envMasked) { Add-Block 'agent_env_content (token masked)' @($envMasked -split "`r?`n") }
|
|
693
|
+
Add-Out ''
|
|
694
|
+
Add-Out '[heartbeat]'
|
|
695
|
+
Add-Field 'heartbeat_path' $heartbeatPath
|
|
696
|
+
Add-Field 'heartbeat_present' $heartbeat.Present
|
|
697
|
+
Add-Field 'heartbeat_parsed' $heartbeat.Parsed
|
|
698
|
+
Add-Field 'heartbeat_age_seconds' $heartbeat.AgeSeconds
|
|
699
|
+
Add-Field 'heartbeat_fresh' $heartbeat.Fresh
|
|
700
|
+
Add-Field 'heartbeat_detail' $heartbeat.Detail
|
|
701
|
+
if ($heartbeat.Json) { Add-Block 'heartbeat_json' @($heartbeat.Json -split "`r?`n") }
|
|
702
|
+
Add-Out ''
|
|
703
|
+
Add-Out '[agent.log]'
|
|
704
|
+
Add-Field 'agent_log_path' $agentLogPath
|
|
705
|
+
Add-Field 'agent_log_present' $log.Present
|
|
706
|
+
Add-Field 'agent_log_bytes' $log.Bytes
|
|
707
|
+
Add-Field 'agent_log_last_write_utc' $log.LastWriteTime
|
|
708
|
+
Add-Field 'torn-line' $log.TornLine
|
|
709
|
+
if ($log.Error) { Add-Field -Name 'agent_log_read_error' -Value $log.Error }
|
|
710
|
+
Add-Block ('agent_log_tail (last ' + $logLines + ' lines)') @($log.Tail)
|
|
711
|
+
Add-Out ''
|
|
712
|
+
Add-Out '[agent process]'
|
|
713
|
+
Add-Field 'core_agent_processes' $processes.Count
|
|
714
|
+
foreach ($proc in $processes) { Add-Field -Name ('pid ' + $proc.Pid) -Value $proc.CommandLine }
|
|
715
|
+
Add-Out ''
|
|
716
|
+
Add-Out '[gateway health]'
|
|
717
|
+
Add-Field 'health_url' $health.Url
|
|
718
|
+
Add-Field 'health_checked' $health.Checked
|
|
719
|
+
Add-Field -Name 'health_detail' -Value $health.Detail
|
|
720
|
+
if ($health.Json) { Add-Block 'health_json' @($health.Json -split "`r?`n") }
|
|
721
|
+
Add-Out ''
|
|
722
|
+
Add-Out '[problems]'
|
|
723
|
+
if ($problems.Count -eq 0) { Add-Out ' (none)' } else { foreach ($problem in $problems) { Add-Out (' - ' + $problem) } }
|
|
724
|
+
Add-Out ''
|
|
725
|
+
Add-Out '[verdict]'
|
|
726
|
+
Add-Field 'interpreter_ok' $interpreterOk
|
|
727
|
+
Add-Field 'autostart_ok' $autostartOk
|
|
728
|
+
Add-Field 'heartbeat_fresh' $heartbeat.Fresh
|
|
729
|
+
Add-Field 'node_online' $health.Online
|
|
730
|
+
|
|
731
|
+
foreach ($line in $out) { Write-Host $line }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if ($healthy) { exit 0 }
|
|
735
|
+
exit 2
|