gemini-computer-use 0.2.4 → 0.4.2
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 +741 -24
- package/bin/cli.js +112 -17
- 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 +8 -3
- package/skills/orchestrator.md +12 -0
package/ops/update.ps1
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# Antigravity Mesh - Windows node updater (single shot)
|
|
3
|
+
#
|
|
4
|
+
# Meant to be run by the Scheduled Task "AntigravityMeshUpdater" once a day, and
|
|
5
|
+
# by hand through "gemini-computer-use update". It answers one question: is a
|
|
6
|
+
# newer release published, and - unless -Check was given - install it.
|
|
7
|
+
#
|
|
8
|
+
# WHY A POWERSHELL WRAPPER
|
|
9
|
+
# The updater itself is Python (core/updater.py), because it has to work on
|
|
10
|
+
# Linux and macOS too. This wrapper exists for the two things only Windows
|
|
11
|
+
# knows: which interpreter the node is actually pinned to (the Startup .vbs),
|
|
12
|
+
# and where the payload lives (WshShell.CurrentDirectory). Starting the updater
|
|
13
|
+
# with the WRONG interpreter - the Microsoft Store alias - was the failure that
|
|
14
|
+
# kept a node offline for a day, so the interpreter is taken from the same
|
|
15
|
+
# launcher the agent itself is started from and refused if it is an alias.
|
|
16
|
+
#
|
|
17
|
+
# WHAT IT DOES, IN ORDER
|
|
18
|
+
# 1. resolves the install directory (-InstallDir, the Startup launcher, this
|
|
19
|
+
# script's own location, the config directory - in that order) and refuses
|
|
20
|
+
# a directory that holds no core\agent.py;
|
|
21
|
+
# 2. resolves the interpreter (-Python, the Startup launcher's Run line, the
|
|
22
|
+
# heartbeat, MESH_PYTHON, then a real python.exe on PATH) and refuses the
|
|
23
|
+
# Microsoft Store alias, which never runs Python;
|
|
24
|
+
# 3. runs "python -m core.updater --check" (with -Check) or "--apply",
|
|
25
|
+
# capturing stdout and stderr separately so the JSON answer stays parseable
|
|
26
|
+
# on Windows PowerShell 5.1;
|
|
27
|
+
# 4. prints (or logs, with -Quiet) the local version, the newest published
|
|
28
|
+
# version and what was done.
|
|
29
|
+
#
|
|
30
|
+
# EXIT CODES
|
|
31
|
+
# 0 the node is on the newest release (or was updated without a restart)
|
|
32
|
+
# 2 an update is available and was not installed (-Check), or the node is
|
|
33
|
+
# misconfigured (no payload, no usable interpreter)
|
|
34
|
+
# 3 the update failed: network, checksum mismatch, unusable payload
|
|
35
|
+
# 4 the update was installed and the node is restarting
|
|
36
|
+
#
|
|
37
|
+
# PARAMETERS
|
|
38
|
+
# -Check report only; never download or replace anything
|
|
39
|
+
# -Force ignore the check interval and the failed-attempt backoff
|
|
40
|
+
# -NoRestart install the payload but do not restart the agent
|
|
41
|
+
# -Offline with -Check: report the cached answer, touch no network
|
|
42
|
+
# -Json print the updater's JSON answer verbatim (for scripts)
|
|
43
|
+
# -InstallDir payload directory override
|
|
44
|
+
# -Python interpreter override
|
|
45
|
+
# -ConfigDir where agent.env / agent.heartbeat live.
|
|
46
|
+
# Default: %USERPROFILE%\.config\antigravity-mesh
|
|
47
|
+
# -LogFile this script's journal. Default: <ConfigDir>\update-task.log
|
|
48
|
+
# -Quiet write to -LogFile only, print nothing to the console
|
|
49
|
+
#
|
|
50
|
+
# This file is deliberately ASCII-only: Windows PowerShell 5.1 reads a BOM-less
|
|
51
|
+
# script with the ANSI code page, so non-ASCII text here is a portability bug.
|
|
52
|
+
#
|
|
53
|
+
# Usage:
|
|
54
|
+
# .\update.ps1
|
|
55
|
+
# .\update.ps1 -Check
|
|
56
|
+
# .\update.ps1 -Quiet -LogFile C:\logs\update.log
|
|
57
|
+
# ==============================================================================
|
|
58
|
+
[CmdletBinding()]
|
|
59
|
+
param(
|
|
60
|
+
[switch]$Check,
|
|
61
|
+
[switch]$Force,
|
|
62
|
+
[switch]$NoRestart,
|
|
63
|
+
[switch]$Offline,
|
|
64
|
+
[switch]$Json,
|
|
65
|
+
[string]$InstallDir = '',
|
|
66
|
+
[string]$Python = '',
|
|
67
|
+
[string]$ConfigDir = '',
|
|
68
|
+
[string]$LogFile = '',
|
|
69
|
+
[switch]$Quiet
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
$ErrorActionPreference = 'Continue'
|
|
73
|
+
$WindowsAppsMarker = '\WindowsApps\'
|
|
74
|
+
|
|
75
|
+
$script:QuietMode = [bool]$Quiet
|
|
76
|
+
$script:LogTarget = ''
|
|
77
|
+
$script:Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList @($false)
|
|
78
|
+
|
|
79
|
+
function Write-LogLine {
|
|
80
|
+
param([string]$Text)
|
|
81
|
+
if ([string]::IsNullOrEmpty($script:LogTarget)) { return }
|
|
82
|
+
try {
|
|
83
|
+
$stamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssK')
|
|
84
|
+
[System.IO.File]::AppendAllText($script:LogTarget, "[$stamp] $Text" + [Environment]::NewLine, $script:Utf8NoBom)
|
|
85
|
+
} catch { }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Write-Note {
|
|
89
|
+
param([string]$Text)
|
|
90
|
+
Write-LogLine $Text
|
|
91
|
+
if (-not $script:QuietMode) { Write-Host $Text }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Stop-WithConfigError {
|
|
95
|
+
param([string]$Message)
|
|
96
|
+
Write-Note ('CONFIG ERROR: ' + $Message)
|
|
97
|
+
exit 2
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Resolve-FullPath {
|
|
101
|
+
param([string]$Path)
|
|
102
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return $Path }
|
|
103
|
+
try { return [System.IO.Path]::GetFullPath($Path) } catch { return $Path }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Test-File {
|
|
107
|
+
# Test-Path refuses an empty -LiteralPath with a parameter binding error, and
|
|
108
|
+
# that error aborts the whole statement it sits in - including the body of the
|
|
109
|
+
# "if (Test-Path $x)" it guards. Every path check in this script goes through
|
|
110
|
+
# here, so an unresolved value is simply "not a file".
|
|
111
|
+
param([string]$Path)
|
|
112
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
|
|
113
|
+
return (Test-Path -LiteralPath $Path -PathType Leaf)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function Read-TextFile {
|
|
117
|
+
param([string]$Path)
|
|
118
|
+
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
|
119
|
+
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
|
120
|
+
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
|
121
|
+
}
|
|
122
|
+
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
|
123
|
+
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
|
124
|
+
}
|
|
125
|
+
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
|
126
|
+
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
|
127
|
+
}
|
|
128
|
+
# The launcher is written as UTF-16 ("Unicode") by install.ps1, but a
|
|
129
|
+
# hand-edited file may be ANSI or UTF-8, so a NUL byte decides.
|
|
130
|
+
$hasNul = $false
|
|
131
|
+
$limit = [Math]::Min($bytes.Length, 512)
|
|
132
|
+
for ($i = 0; $i -lt $limit; $i++) {
|
|
133
|
+
if ($bytes[$i] -eq 0) { $hasNul = $true; break }
|
|
134
|
+
}
|
|
135
|
+
if ($hasNul) { return [System.Text.Encoding]::Unicode.GetString($bytes) }
|
|
136
|
+
return [System.Text.Encoding]::UTF8.GetString($bytes)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------ paths ----
|
|
140
|
+
if ([string]::IsNullOrWhiteSpace($ConfigDir)) {
|
|
141
|
+
$profileDir = $env:USERPROFILE
|
|
142
|
+
if ([string]::IsNullOrWhiteSpace($profileDir)) { $profileDir = [Environment]::GetFolderPath('UserProfile') }
|
|
143
|
+
if ([string]::IsNullOrWhiteSpace($profileDir)) {
|
|
144
|
+
Stop-WithConfigError 'cannot resolve the user profile directory; pass -ConfigDir explicitly'
|
|
145
|
+
}
|
|
146
|
+
$ConfigDir = Join-Path $profileDir '.config\antigravity-mesh'
|
|
147
|
+
}
|
|
148
|
+
$ConfigDir = Resolve-FullPath $ConfigDir
|
|
149
|
+
if ([string]::IsNullOrWhiteSpace($LogFile)) { $LogFile = Join-Path $ConfigDir 'update-task.log' }
|
|
150
|
+
$LogFile = Resolve-FullPath $LogFile
|
|
151
|
+
try {
|
|
152
|
+
$logDir = Split-Path -Parent $LogFile
|
|
153
|
+
if ($logDir -and -not (Test-Path -LiteralPath $logDir -PathType Container)) {
|
|
154
|
+
$null = New-Item -ItemType Directory -Path $logDir -Force -ErrorAction Stop
|
|
155
|
+
}
|
|
156
|
+
$script:LogTarget = $LogFile
|
|
157
|
+
} catch {
|
|
158
|
+
$script:LogTarget = ''
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------- the launcher --
|
|
162
|
+
# install.ps1 writes this file; it names both the interpreter to run and the
|
|
163
|
+
# directory the agent runs from, which is exactly what the updater needs.
|
|
164
|
+
$startupDir = [Environment]::GetFolderPath('Startup')
|
|
165
|
+
$vbsPath = ''
|
|
166
|
+
$launcherText = ''
|
|
167
|
+
if (-not [string]::IsNullOrWhiteSpace($startupDir)) {
|
|
168
|
+
$vbsPath = Join-Path $startupDir 'antigravity-agent.vbs'
|
|
169
|
+
if (Test-Path -LiteralPath $vbsPath -PathType Leaf) {
|
|
170
|
+
try { $launcherText = Read-TextFile $vbsPath } catch { $launcherText = '' }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Get-LauncherInterpreter {
|
|
175
|
+
if ([string]::IsNullOrWhiteSpace($launcherText)) { return '' }
|
|
176
|
+
$run = [regex]::Match($launcherText, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?cmd[ \t]+/c[ \t]+"+(?<exe>[^"]+?)"+[ \t]+(?:-u[ \t]+)?-m[ \t]+core\.agent[^\r\n]*')
|
|
177
|
+
if (-not $run.Success) {
|
|
178
|
+
$run = [regex]::Match($launcherText, '(?im)^[^\r\n]*WshShell\.Run[^\r\n]*?(?<exe>[A-Za-z]:\\[^"]*?\.(?:exe|cmd|bat))')
|
|
179
|
+
}
|
|
180
|
+
if ($run.Success) { return $run.Groups['exe'].Value.Trim() }
|
|
181
|
+
return ''
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function Get-LauncherPayloadDir {
|
|
185
|
+
if ([string]::IsNullOrWhiteSpace($launcherText)) { return '' }
|
|
186
|
+
$dir = [regex]::Match($launcherText, '(?im)^[^\r\n]*WshShell\.CurrentDirectory[ \t]*=[ \t]*"(?<dir>[^"]+)"')
|
|
187
|
+
if ($dir.Success) { return $dir.Groups['dir'].Value.Trim() }
|
|
188
|
+
return ''
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function Test-NodePayload {
|
|
192
|
+
param([string]$Path)
|
|
193
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
|
|
194
|
+
return (Test-Path -LiteralPath (Join-Path $Path 'core\agent.py') -PathType Leaf)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# --- install directory --------------------------------------------------------
|
|
198
|
+
$PayloadReason = '-InstallDir'
|
|
199
|
+
if (-not (Test-NodePayload $InstallDir)) {
|
|
200
|
+
$candidate = Get-LauncherPayloadDir
|
|
201
|
+
if (Test-NodePayload $candidate) {
|
|
202
|
+
$InstallDir = $candidate
|
|
203
|
+
$PayloadReason = 'the Startup launcher (WshShell.CurrentDirectory)'
|
|
204
|
+
} else {
|
|
205
|
+
$candidate = ''
|
|
206
|
+
if ($PSScriptRoot) { $candidate = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) }
|
|
207
|
+
if (Test-NodePayload $candidate) {
|
|
208
|
+
$InstallDir = $candidate
|
|
209
|
+
$PayloadReason = 'the location of this script'
|
|
210
|
+
} elseif (Test-NodePayload $ConfigDir) {
|
|
211
|
+
$InstallDir = $ConfigDir
|
|
212
|
+
$PayloadReason = 'the config directory'
|
|
213
|
+
} else {
|
|
214
|
+
Stop-WithConfigError ('no node payload found: -InstallDir, the Startup launcher and the script location all lack core\agent.py')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
$InstallDir = Resolve-FullPath $InstallDir
|
|
219
|
+
if (-not (Test-NodePayload $InstallDir)) {
|
|
220
|
+
Stop-WithConfigError ('the install directory holds no core\agent.py: ' + $InstallDir)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# --- interpreter --------------------------------------------------------------
|
|
224
|
+
function Get-HeartbeatInterpreter {
|
|
225
|
+
$heartbeat = Join-Path $ConfigDir 'agent.heartbeat'
|
|
226
|
+
if (-not (Test-Path -LiteralPath $heartbeat -PathType Leaf)) { return '' }
|
|
227
|
+
try {
|
|
228
|
+
$json = [System.IO.File]::ReadAllText($heartbeat) | ConvertFrom-Json
|
|
229
|
+
} catch { return '' }
|
|
230
|
+
$prop = $json.PSObject.Properties['interpreter']
|
|
231
|
+
if ($prop -and $prop.Value) { return ([string]$prop.Value).Trim() }
|
|
232
|
+
return ''
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function Get-PythonOnPath {
|
|
236
|
+
# The first "python" on PATH is usually the Microsoft Store alias, so every
|
|
237
|
+
# candidate is checked and the alias is skipped rather than accepted.
|
|
238
|
+
foreach ($cmd in @(Get-Command python -All -ErrorAction SilentlyContinue)) {
|
|
239
|
+
if ($cmd -and $cmd.Source -and $cmd.Source -notlike "*$WindowsAppsMarker*" -and (Test-Path -LiteralPath $cmd.Source -PathType Leaf)) {
|
|
240
|
+
return $cmd.Source
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
$candidates = @(
|
|
244
|
+
"$env:LOCALAPPDATA\Programs\Python\Python313\python.exe",
|
|
245
|
+
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
|
|
246
|
+
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
|
|
247
|
+
"$env:ProgramFiles\Python313\python.exe",
|
|
248
|
+
"$env:ProgramFiles\Python312\python.exe",
|
|
249
|
+
"$env:ProgramFiles\Python311\python.exe"
|
|
250
|
+
)
|
|
251
|
+
foreach ($candidate in $candidates) {
|
|
252
|
+
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
|
|
253
|
+
}
|
|
254
|
+
return ''
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
$InterpreterReason = '-Python'
|
|
258
|
+
if (-not (Test-File $Python)) {
|
|
259
|
+
$Python = ''
|
|
260
|
+
$fromLauncher = Get-LauncherInterpreter
|
|
261
|
+
if ($fromLauncher) {
|
|
262
|
+
$Python = $fromLauncher
|
|
263
|
+
$InterpreterReason = 'the Startup launcher (WshShell.Run)'
|
|
264
|
+
}
|
|
265
|
+
if (-not (Test-File $Python)) {
|
|
266
|
+
$Python = Get-HeartbeatInterpreter
|
|
267
|
+
$InterpreterReason = 'the heartbeat'
|
|
268
|
+
}
|
|
269
|
+
if (-not (Test-File $Python)) {
|
|
270
|
+
$Python = $env:MESH_PYTHON
|
|
271
|
+
$InterpreterReason = 'MESH_PYTHON'
|
|
272
|
+
}
|
|
273
|
+
if (-not (Test-File $Python)) {
|
|
274
|
+
$Python = Get-PythonOnPath
|
|
275
|
+
$InterpreterReason = 'PATH'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if ([string]::IsNullOrWhiteSpace($Python)) {
|
|
279
|
+
Stop-WithConfigError 'no python.exe found: pass -Python, or re-run install.ps1 so the node pins a real interpreter'
|
|
280
|
+
}
|
|
281
|
+
if ($Python -like "*$WindowsAppsMarker*") {
|
|
282
|
+
Stop-WithConfigError ('the resolved interpreter is the Microsoft Store alias: ' + $Python + ' - that alias never runs Python; re-run install.ps1')
|
|
283
|
+
}
|
|
284
|
+
if (-not (Test-File $Python)) {
|
|
285
|
+
Stop-WithConfigError ('the resolved interpreter does not exist: ' + $Python)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
Write-Note ('updater start: install=' + $InstallDir + ' (' + $PayloadReason + ') interpreter=' + $Python + ' (' + $InterpreterReason + ') mode=' + $(if ($Check) { 'check' } else { 'apply' }))
|
|
289
|
+
|
|
290
|
+
# --------------------------------------------------------------- run Python --
|
|
291
|
+
# stdout and stderr are redirected to files by cmd.exe, not captured by
|
|
292
|
+
# Start-Process: on Windows PowerShell 5.1 a process started that way reports an
|
|
293
|
+
# empty ExitCode once it has exited, and mixing stderr into stdout would break
|
|
294
|
+
# the JSON answer the updater prints.
|
|
295
|
+
$outFile = Join-Path $env:TEMP ('mesh-update-out-' + [Guid]::NewGuid().ToString('N') + '.json')
|
|
296
|
+
$errFile = Join-Path $env:TEMP ('mesh-update-err-' + [Guid]::NewGuid().ToString('N') + '.log')
|
|
297
|
+
if ($Check) { $mode = '--check' } else { $mode = '--apply' }
|
|
298
|
+
$arguments = @('-m', 'core.updater', $mode, '--json', '--dir', $InstallDir)
|
|
299
|
+
if ($Force) { $arguments += '--force' }
|
|
300
|
+
if ($Offline -and $Check) { $arguments += '--offline' }
|
|
301
|
+
if ($NoRestart -and -not $Check) { $arguments += '--no-restart' }
|
|
302
|
+
|
|
303
|
+
$quoted = @($arguments | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join ' '
|
|
304
|
+
$commandLine = '/c ""' + $Python + '" ' + $quoted + ' > "' + $outFile + '" 2> "' + $errFile + '""'
|
|
305
|
+
$exitCode = 3
|
|
306
|
+
try {
|
|
307
|
+
$null = & cmd.exe $commandLine
|
|
308
|
+
$exitCode = $LASTEXITCODE
|
|
309
|
+
} catch {
|
|
310
|
+
Write-Note ('could not start the interpreter: ' + $_.Exception.Message)
|
|
311
|
+
exit 3
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
$answer = ''
|
|
315
|
+
if (Test-Path -LiteralPath $outFile) {
|
|
316
|
+
try { $answer = ([System.IO.File]::ReadAllText($outFile)).Trim() } catch { $answer = '' }
|
|
317
|
+
}
|
|
318
|
+
$errors = ''
|
|
319
|
+
if (Test-Path -LiteralPath $errFile) {
|
|
320
|
+
try { $errors = ([System.IO.File]::ReadAllText($errFile)).Trim() } catch { $errors = '' }
|
|
321
|
+
}
|
|
322
|
+
Remove-Item -LiteralPath $outFile, $errFile -Force -ErrorAction SilentlyContinue
|
|
323
|
+
|
|
324
|
+
$parsed = $null
|
|
325
|
+
if ($answer) {
|
|
326
|
+
try { $parsed = $answer | ConvertFrom-Json } catch { $parsed = $null }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# The updater's own log lines (a traceback, a watchdog note) belong in the
|
|
330
|
+
# journal: they are the only trace when --apply fails before printing JSON.
|
|
331
|
+
if ($errors) {
|
|
332
|
+
foreach ($line in @($errors -split "`r?`n")) {
|
|
333
|
+
if ($line.Trim()) { Write-LogLine ('python: ' + $line.Trim()) }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if ($parsed) {
|
|
338
|
+
$current = [string]$parsed.current
|
|
339
|
+
$latest = [string]$parsed.latest
|
|
340
|
+
$reason = [string]$parsed.reason
|
|
341
|
+
if ($Json) {
|
|
342
|
+
# The updater's own answer, verbatim: scripts read this, so it is not
|
|
343
|
+
# decorated with the human summary below.
|
|
344
|
+
Write-Output $answer
|
|
345
|
+
} else {
|
|
346
|
+
if ($Check) {
|
|
347
|
+
if ($exitCode -eq 2) {
|
|
348
|
+
Write-Note ('update available: ' + $current + ' -> ' + $latest + ' (' + $reason + ')')
|
|
349
|
+
} elseif ($exitCode -eq 0) {
|
|
350
|
+
Write-Note ('up to date: ' + $current + ' (' + $reason + ')')
|
|
351
|
+
} else {
|
|
352
|
+
Write-Note ('check failed: ' + $reason)
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
if ($exitCode -eq 0 -or $exitCode -eq 4) {
|
|
356
|
+
Write-Note ('updated: ' + $reason + ' (exit ' + $exitCode + ')')
|
|
357
|
+
} else {
|
|
358
|
+
Write-Note ('update failed: ' + $reason)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (-not $script:QuietMode) {
|
|
362
|
+
if ($latest) { Write-Host ('latest : ' + $latest) }
|
|
363
|
+
if ($reason) { Write-Host ('detail : ' + $reason) }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} elseif (-not $script:QuietMode -and -not $Json) {
|
|
367
|
+
Write-Host ('the updater produced no JSON answer (exit ' + $exitCode + ')')
|
|
368
|
+
if ($errors) { Write-Host $errors }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if ($Check -and $exitCode -eq 0 -and $parsed -and $parsed.update_available) { exit 2 }
|
|
372
|
+
if ($exitCode -eq 2 -and -not $Check) { exit 2 }
|
|
373
|
+
exit $exitCode
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Windows agent watchdog and doctor
|
|
2
|
+
|
|
3
|
+
| File | Role |
|
|
4
|
+
| :--- | :--- |
|
|
5
|
+
| `agent-watchdog.ps1` | single-shot supervisor: check the node, restart the agent if it is dead |
|
|
6
|
+
| `../doctor.ps1` | read-only diagnostic: report every fact that explains an offline node |
|
|
7
|
+
| `../update.ps1` | single-shot updater: install a newer release and let the node restart onto it |
|
|
8
|
+
| `run-hidden.vbs` | the windowless launcher both scheduled tasks go through: no console is ever drawn |
|
|
9
|
+
|
|
10
|
+
All three PowerShell scripts are ASCII-only, target Windows PowerShell 5.1 and use
|
|
11
|
+
nothing but in-box cmdlets; `run-hidden.vbs` is ASCII-only too and needs only
|
|
12
|
+
`wscript.exe`. None of them needs the task scheduler to be running in order to work.
|
|
13
|
+
|
|
14
|
+
## Why `run-hidden.vbs` exists
|
|
15
|
+
|
|
16
|
+
A scheduled task has no console of its own. When the action is `powershell.exe`,
|
|
17
|
+
Windows therefore allocates a **new** console for it and only then applies
|
|
18
|
+
`-WindowStyle Hidden`: the window is created, drawn and hidden again, and the user
|
|
19
|
+
sees a black window appear and disappear. The watchdog fires every five minutes, so
|
|
20
|
+
that flash is the most noticeable thing this project does on a desktop.
|
|
21
|
+
|
|
22
|
+
`wscript.exe` is a GUI host and never allocates a console, and
|
|
23
|
+
`WshShell.Run command, 0, False` hands the child `SW_HIDE` in its `STARTUPINFO` —
|
|
24
|
+
the window is hidden from the first moment, so nothing is ever painted. That is the
|
|
25
|
+
same mechanism as the logon launcher (`antigravity-agent.vbs`), which is why a node
|
|
26
|
+
starts without a window either. Output goes to
|
|
27
|
+
`<ConfigDir>\<script name>.out.log`, because a window that never appears also never
|
|
28
|
+
shows an error.
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
# exactly how the two tasks call it; the script path is relative to the payload
|
|
32
|
+
wscript.exe "$Payload\ops\windows\run-hidden.vbs" "ops\windows\agent-watchdog.ps1" -Quiet
|
|
33
|
+
wscript.exe "$Payload\ops\windows\run-hidden.vbs" "ops\update.ps1" -Quiet
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why they exist
|
|
37
|
+
|
|
38
|
+
After a Windows Update reboot the logon launcher did nothing, and the node stayed
|
|
39
|
+
offline for about 29 hours. The interpreter pinned in the Startup `.vbs` was the
|
|
40
|
+
Microsoft Store App Execution Alias `<...>\WindowsApps\python.exe` - a file that
|
|
41
|
+
exists and starts, but runs no Python at all. The only trace was a truncated
|
|
42
|
+
`Python ` fragment appended to `agent.log` with no trailing newline.
|
|
43
|
+
|
|
44
|
+
The watchdog therefore **refuses any interpreter whose path contains
|
|
45
|
+
`\WindowsApps\`**, and the doctor surfaces the same fact (plus the torn log line)
|
|
46
|
+
within seconds.
|
|
47
|
+
|
|
48
|
+
## `agent-watchdog.ps1` - single-shot supervisor
|
|
49
|
+
|
|
50
|
+
Run every 5 minutes by the Scheduled Task **`AntigravityMeshWatchdog`**.
|
|
51
|
+
|
|
52
|
+
| Parameter | Default | Meaning |
|
|
53
|
+
| :--- | :--- | :--- |
|
|
54
|
+
| `-ConfigDir` | `%USERPROFILE%\.config\antigravity-mesh` | where `agent.env`, `agent.log`, `agent.heartbeat` live |
|
|
55
|
+
| `-LogFile` | `<ConfigDir>\watchdog.log` | the watchdog's own journal |
|
|
56
|
+
| `-GraceSeconds` | `60` | a heartbeat younger than this counts as alive |
|
|
57
|
+
| `-Quiet` | off | log only, no console output |
|
|
58
|
+
|
|
59
|
+
Liveness is checked in this order, and the first positive answer wins:
|
|
60
|
+
|
|
61
|
+
1. `<ConfigDir>\agent.heartbeat` is valid JSON and its `ts` is newer than
|
|
62
|
+
`-GraceSeconds`;
|
|
63
|
+
2. `GET https://<gateway>/health?user=<node>` reports `node_online: true`;
|
|
64
|
+
3. a verified agent process is running: a `python.exe`/`pythonw.exe`/`py.exe` with
|
|
65
|
+
`-m core.agent` as a real argument, or the `cmd.exe` launcher wrapper that also
|
|
66
|
+
carries the `agent.log` redirect. A process that merely mentions `core.agent`
|
|
67
|
+
(a PowerShell session, a grep) never counts - otherwise a dead node would look
|
|
68
|
+
alive and never be restarted.
|
|
69
|
+
|
|
70
|
+
When the node is dead it appends **exactly one** diagnostic line to `agent.log`
|
|
71
|
+
(reason + interpreter being started), starts the agent hidden with the same
|
|
72
|
+
command line, environment and working directory as the `.vbs`
|
|
73
|
+
(`cmd /c ""<python.exe>" -u -m core.agent >> "<agent.log>" 2>&1`), waits up to 20 s
|
|
74
|
+
for liveness and journals the outcome.
|
|
75
|
+
|
|
76
|
+
| Exit code | Meaning |
|
|
77
|
+
| :--- | :--- |
|
|
78
|
+
| `0` | the node is online (already, or recovered by this run); also when another watchdog instance is handling it |
|
|
79
|
+
| `2` | configuration or launcher problem: `agent.env`, the Startup `.vbs` or the pinned interpreter is missing, unusable, or is the Store alias |
|
|
80
|
+
| `3` | the node is still offline after the recovery attempt |
|
|
81
|
+
|
|
82
|
+
It is idempotent: a live node (or a live `core.agent` process) is never started
|
|
83
|
+
twice, and a named mutex serialises overlapping runs. The token is never printed
|
|
84
|
+
and never logged - it is masked as `****`.
|
|
85
|
+
|
|
86
|
+
## `../doctor.ps1` - read-only diagnostic
|
|
87
|
+
|
|
88
|
+
| Parameter | Default | Meaning |
|
|
89
|
+
| :--- | :--- | :--- |
|
|
90
|
+
| `-ConfigDir` | `%USERPROFILE%\.config\antigravity-mesh` | same directory as above |
|
|
91
|
+
| `-Json` | off | print exactly one JSON object and nothing else |
|
|
92
|
+
| `-TailLines` | `20` | how many `agent.log` lines to show |
|
|
93
|
+
|
|
94
|
+
It reports the interpreter from the `.vbs` (with `--version` and an
|
|
95
|
+
`import websockets` probe, and whether it is the Store alias), the `.vbs` path and
|
|
96
|
+
content with the token masked, the state of `AntigravityMeshWatchdog`, `agent.env`
|
|
97
|
+
with the token masked, the heartbeat JSON and its age, the last `-TailLines` lines
|
|
98
|
+
of `agent.log` with `torn-line: true` when the file does not end in a newline, the
|
|
99
|
+
gateway `/health` JSON, and whether a verified `core.agent` process is running
|
|
100
|
+
(the same rule the watchdog applies: a Python interpreter running `-m core.agent`,
|
|
101
|
+
never a process that merely mentions it).
|
|
102
|
+
|
|
103
|
+
It finishes with the verdict lines `interpreter_ok`, `autostart_ok`,
|
|
104
|
+
`heartbeat_fresh`, `node_online`. Exit code `0` means all of `interpreter_ok`,
|
|
105
|
+
`autostart_ok` and `node_online` are true; anything else is `2`. Nothing is
|
|
106
|
+
written to disk.
|
|
107
|
+
|
|
108
|
+
## Running them by hand
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
# from the payload / repository root
|
|
112
|
+
.\ops\doctor.ps1
|
|
113
|
+
.\ops\doctor.ps1 -Json
|
|
114
|
+
.\ops\doctor.ps1 -TailLines 50
|
|
115
|
+
.\ops\windows\agent-watchdog.ps1
|
|
116
|
+
.\ops\windows\agent-watchdog.ps1 -Quiet
|
|
117
|
+
|
|
118
|
+
# through the CLI (same scripts, found next to bin/cli.js)
|
|
119
|
+
npx gemini-computer-use doctor
|
|
120
|
+
npx gemini-computer-use doctor --json
|
|
121
|
+
npx gemini-computer-use restart
|
|
122
|
+
|
|
123
|
+
# the scheduled task
|
|
124
|
+
Get-ScheduledTask -TaskName AntigravityMeshWatchdog
|
|
125
|
+
Get-ScheduledTaskInfo -TaskName AntigravityMeshWatchdog
|
|
126
|
+
Start-ScheduledTask -TaskName AntigravityMeshWatchdog
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## What the installer must register
|
|
130
|
+
|
|
131
|
+
Task name: **`AntigravityMeshWatchdog`**.
|
|
132
|
+
|
|
133
|
+
* **Action**: `wscript.exe`
|
|
134
|
+
* **Arguments**: `"<payload>\ops\windows\run-hidden.vbs" "ops\windows\agent-watchdog.ps1" -Quiet`
|
|
135
|
+
* **Working directory**: the payload directory (the directory that contains
|
|
136
|
+
`core\`, next to `install.ps1`). The launcher resolves the script against its own
|
|
137
|
+
location, so the registered path survives a payload that moves.
|
|
138
|
+
* **Triggers**: once at logon of the installing user, plus a repeating trigger
|
|
139
|
+
(`-Once -RepetitionInterval 5 minutes`, indefinite duration). The logon trigger
|
|
140
|
+
mirrors the `.vbs`; the repeating trigger is what actually recovers the node, and
|
|
141
|
+
`-StartWhenAvailable` makes up a run missed during a reboot.
|
|
142
|
+
* **Settings**: `-MultipleInstances IgnoreNew`, `-StartWhenAvailable`,
|
|
143
|
+
`-ExecutionTimeLimit 10 minutes`.
|
|
144
|
+
|
|
145
|
+
```powershell
|
|
146
|
+
$PayloadDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
147
|
+
$Launcher = Join-Path $PayloadDir 'ops\windows\run-hidden.vbs'
|
|
148
|
+
$action = New-ScheduledTaskAction -Execute 'wscript.exe' `
|
|
149
|
+
-Argument ('"' + $Launcher + '" "ops\windows\agent-watchdog.ps1" -Quiet') `
|
|
150
|
+
-WorkingDirectory $PayloadDir
|
|
151
|
+
$atLogon = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
|
|
152
|
+
$repeat = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(2) `
|
|
153
|
+
-RepetitionInterval (New-TimeSpan -Minutes 5) `
|
|
154
|
+
-RepetitionDuration ([TimeSpan]::MaxValue)
|
|
155
|
+
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
|
|
156
|
+
-DontStopIfGoingOnBatteries -StartWhenAvailable `
|
|
157
|
+
-MultipleInstances IgnoreNew `
|
|
158
|
+
-ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
|
159
|
+
Register-ScheduledTask -TaskName 'AntigravityMeshWatchdog' -Action $action `
|
|
160
|
+
-Trigger @($atLogon, $repeat) -Settings $settings -Force `
|
|
161
|
+
-Description 'Antigravity Mesh: keep the node agent running (every 5 minutes).'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The `ops\` directory must travel with the payload: it has to be listed in
|
|
165
|
+
`package.json` (`files`), in `build-installer-exe.ps1` (`$PAYLOAD`) and in
|
|
166
|
+
whatever `deploy_gateway.sh` stages for the node bootstrap.
|
|
167
|
+
|
|
168
|
+
## `../update.ps1` - single-shot self-update
|
|
169
|
+
|
|
170
|
+
Run once a day by the Scheduled Task **`AntigravityMeshUpdater`**, and by hand as
|
|
171
|
+
`gemini-computer-use update [-Check]`.
|
|
172
|
+
|
|
173
|
+
| Parameter | Default | Meaning |
|
|
174
|
+
| :--- | :--- | :--- |
|
|
175
|
+
| `-Check` | off | report only; never download or replace anything |
|
|
176
|
+
| `-Force` | off | ignore the check interval and the failed-attempt backoff |
|
|
177
|
+
| `-NoRestart` | off | install the payload but do not restart the agent |
|
|
178
|
+
| `-Offline` | off | with `-Check`: report the cached answer, touch no network |
|
|
179
|
+
| `-Json` | off | print the updater's JSON answer verbatim |
|
|
180
|
+
| `-InstallDir` | from the Startup `.vbs` | payload directory |
|
|
181
|
+
| `-Python` | from the Startup `.vbs` | interpreter to run the updater with |
|
|
182
|
+
| `-ConfigDir` | `%USERPROFILE%\.config\antigravity-mesh` | where `agent.env` and `agent.heartbeat` live |
|
|
183
|
+
| `-LogFile` | `<ConfigDir>\update-task.log` | this script's own journal |
|
|
184
|
+
| `-Quiet` | off | log only, no console output |
|
|
185
|
+
|
|
186
|
+
It resolves the install directory and the interpreter the same way the watchdog
|
|
187
|
+
does - from the Startup launcher `install.ps1` wrote - and **refuses the Microsoft
|
|
188
|
+
Store alias** for the same reason: that alias never runs Python. The work itself is
|
|
189
|
+
done by `python -m core.updater` (`--check` or `--apply`), which downloads the
|
|
190
|
+
release payload, verifies its published SHA-256, swaps it in with a rollback backup
|
|
191
|
+
and starts a helper that brings the agent back. See
|
|
192
|
+
[docs/UPDATES.md](../../docs/UPDATES.md).
|
|
193
|
+
|
|
194
|
+
| Exit code | Meaning |
|
|
195
|
+
| :--- | :--- |
|
|
196
|
+
| `0` | the node is on the newest release, or was updated without a restart |
|
|
197
|
+
| `2` | an update exists and was not installed (`-Check`), or the node is misconfigured |
|
|
198
|
+
| `3` | the update failed: network, checksum mismatch, unusable payload |
|
|
199
|
+
| `4` | the update was installed and the node is restarting |
|
|
200
|
+
|
|
201
|
+
Register it as **`AntigravityMeshUpdater`**: action `wscript.exe` (through
|
|
202
|
+
`run-hidden.vbs`, so the daily run never shows a window), arguments
|
|
203
|
+
`"<payload>\ops\windows\run-hidden.vbs" "ops\update.ps1" -Quiet`, working directory
|
|
204
|
+
the payload directory, trigger daily (the installer uses 03:30), settings
|
|
205
|
+
`-MultipleInstances IgnoreNew` and `-StartWhenAvailable` (so a machine that was off
|
|
206
|
+
at 03:30 updates at the next opportunity).
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
# Сторож и доктор для Windows
|
|
211
|
+
|
|
212
|
+
| Файл | Назначение |
|
|
213
|
+
| :--- | :--- |
|
|
214
|
+
| `agent-watchdog.ps1` | однократная проверка: если узел мёртв - поднять агента |
|
|
215
|
+
| `../doctor.ps1` | диагностика только на чтение: показать всё, что объясняет простой |
|
|
216
|
+
|
|
217
|
+
Оба скрипта - ASCII-only, рассчитаны на Windows PowerShell 5.1 и используют
|
|
218
|
+
только встроенные командлеты. Планировщик заданий для их работы не нужен.
|
|
219
|
+
|
|
220
|
+
**Зачем это нужно.** После перезагрузки (Windows Update) автозапуск молча ничего
|
|
221
|
+
не сделал, и узел оставался offline около 29 часов. В `.vbs` был прописан
|
|
222
|
+
интерпретатор-алиас Microsoft Store `<...>\WindowsApps\python.exe`: файл
|
|
223
|
+
существует и запускается, но Python не выполняет. Единственным следствием была
|
|
224
|
+
обрезанная строка `Python ` в `agent.log` без завершающего перевода строки.
|
|
225
|
+
|
|
226
|
+
Поэтому сторож **отказывается работать с интерпретатором, чей путь содержит
|
|
227
|
+
`\WindowsApps\`**, а доктор за секунды показывает и это, и оборванную строку
|
|
228
|
+
журнала.
|
|
229
|
+
|
|
230
|
+
## `agent-watchdog.ps1`
|
|
231
|
+
|
|
232
|
+
Запускается каждые 5 минут заданием планировщика **`AntigravityMeshWatchdog`**.
|
|
233
|
+
|
|
234
|
+
| Параметр | Значение по умолчанию | Смысл |
|
|
235
|
+
| :--- | :--- | :--- |
|
|
236
|
+
| `-ConfigDir` | `%USERPROFILE%\.config\antigravity-mesh` | где лежат `agent.env`, `agent.log`, `agent.heartbeat` |
|
|
237
|
+
| `-LogFile` | `<ConfigDir>\watchdog.log` | собственный журнал сторожа |
|
|
238
|
+
| `-GraceSeconds` | `60` | heartbeat моложе этого возраста считается живым |
|
|
239
|
+
| `-Quiet` | выключен | только журнал, без вывода в консоль |
|
|
240
|
+
|
|
241
|
+
Признаки жизни проверяются по порядку, побеждает первый положительный:
|
|
242
|
+
сердцебиение `agent.heartbeat` (свежее), затем `GET https://<gateway>/health?user=<узел>`
|
|
243
|
+
с `node_online: true`, затем подтверждённый процесс агента: `python.exe`/`pythonw.exe`/`py.exe`
|
|
244
|
+
с настоящим аргументом `-m core.agent` либо обёртка `cmd.exe`, в которой есть
|
|
245
|
+
перенаправление вывода в `agent.log`. Процесс, который лишь упоминает `core.agent`
|
|
246
|
+
(сессия PowerShell, grep), агентом не считается - иначе мёртвый узел выглядел бы
|
|
247
|
+
живым и никогда не перезапускался.
|
|
248
|
+
|
|
249
|
+
Если узел мёртв, сторож дописывает в `agent.log` **ровно одну** строку с причиной
|
|
250
|
+
и путём интерпретатора, скрытно запускает агента ровно так же, как это делает
|
|
251
|
+
`.vbs`, ждёт до 20 секунд и записывает результат в `-LogFile`.
|
|
252
|
+
|
|
253
|
+
Коды возврата: `0` - узел онлайн (был или восстановлен); `2` - проблема
|
|
254
|
+
конфигурации или автозапуска (`agent.env`, `.vbs` или интерпретатор отсутствуют,
|
|
255
|
+
непригодны либо это алиас Store); `3` - узел так и остался offline. Токен никогда
|
|
256
|
+
не печатается и не пишется в журнал: он маскируется как `****`.
|
|
257
|
+
|
|
258
|
+
## `../doctor.ps1`
|
|
259
|
+
|
|
260
|
+
| Параметр | Значение по умолчанию | Смысл |
|
|
261
|
+
| :--- | :--- | :--- |
|
|
262
|
+
| `-ConfigDir` | `%USERPROFILE%\.config\antigravity-mesh` | тот же каталог |
|
|
263
|
+
| `-Json` | выключен | вывести ровно один JSON-объект и ничего больше |
|
|
264
|
+
| `-TailLines` | `20` | сколько последних строк `agent.log` показать |
|
|
265
|
+
|
|
266
|
+
Доктор показывает интерпретатор из `.vbs` (плюс `--version` и проверку
|
|
267
|
+
`import websockets`, плюс признак алиаса Store), путь и содержимое `.vbs` с
|
|
268
|
+
замаскированным токеном, состояние задания `AntigravityMeshWatchdog`, `agent.env`
|
|
269
|
+
с замаскированным токеном, JSON сердцебиения и его возраст, последние строки
|
|
270
|
+
`agent.log` с флагом `torn-line: true` (файл не заканчивается переводом строки),
|
|
271
|
+
JSON ответа `/health` и наличие подтверждённого процесса агента.
|
|
272
|
+
Завершается строки-вердикты
|
|
273
|
+
`interpreter_ok`, `autostart_ok`, `heartbeat_fresh`, `node_online`. Код возврата
|
|
274
|
+
`0` - всё в порядке, иначе `2`. На диск ничего не записывается.
|
|
275
|
+
|
|
276
|
+
## Ручной запуск
|
|
277
|
+
|
|
278
|
+
```powershell
|
|
279
|
+
.\ops\doctor.ps1
|
|
280
|
+
.\ops\doctor.ps1 -Json
|
|
281
|
+
.\ops\windows\agent-watchdog.ps1
|
|
282
|
+
.\ops\windows\agent-watchdog.ps1 -Quiet
|
|
283
|
+
npx gemini-computer-use doctor
|
|
284
|
+
npx gemini-computer-use restart
|
|
285
|
+
Get-ScheduledTaskInfo -TaskName AntigravityMeshWatchdog
|
|
286
|
+
Start-ScheduledTask -TaskName AntigravityMeshWatchdog
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Установщик регистрирует задание **`AntigravityMeshWatchdog`**: действие
|
|
290
|
+
`powershell.exe` с аргументами
|
|
291
|
+
`-NoProfile -ExecutionPolicy Bypass -File "<payload>\ops\windows\agent-watchdog.ps1" -Quiet`,
|
|
292
|
+
рабочий каталог - каталог payload (там, где лежит `core\`), триггеры - при входе
|
|
293
|
+
пользователя и повтор каждые 5 минут без ограничения длительности, настройки -
|
|
294
|
+
`-MultipleInstances IgnoreNew`, `-StartWhenAvailable`,
|
|
295
|
+
`-ExecutionTimeLimit 10 минут`. Готовый вызов `Register-ScheduledTask` приведён в
|
|
296
|
+
английской части выше. Каталог `ops\` должен попадать в payload: его нужно указать
|
|
297
|
+
в `package.json` (`files`), в `build-installer-exe.ps1` (`$PAYLOAD`) и в том, что
|
|
298
|
+
`deploy_gateway.sh` выкладывает для узла.
|