@geekbeer/minion 2.67.1 → 2.68.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/package.json +1 -1
- package/win/lib/process-manager.js +133 -15
- package/win/minion-cli.ps1 +19 -2
- package/win/routes/commands.js +2 -1
- package/win/server.js +14 -0
package/package.json
CHANGED
|
@@ -54,19 +54,114 @@ function detectProcessManager() {
|
|
|
54
54
|
* @param {string} startCmd - Command/script block to start the agent
|
|
55
55
|
* @returns {string} - Path to the generated update script (.ps1)
|
|
56
56
|
*/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Generate a PowerShell snippet that requests graceful shutdown via the agent's
|
|
59
|
+
* HTTP API, then falls back to force-kill. This mirrors Linux's SIGTERM behavior:
|
|
60
|
+
* the agent runs shutdown() → sends offline heartbeat → exits cleanly.
|
|
61
|
+
*
|
|
62
|
+
* Windows has no SIGTERM for console processes, so we use HTTP instead.
|
|
63
|
+
*
|
|
64
|
+
* @param {number} agentPort - The agent's HTTP port (default 8080)
|
|
65
|
+
* @param {string} apiToken - The agent's API token for authentication
|
|
66
|
+
* @returns {string} PowerShell code block for graceful shutdown
|
|
67
|
+
*/
|
|
68
|
+
function buildGracefulStopBlock(agentPort, apiToken) {
|
|
69
|
+
return [
|
|
70
|
+
` try {`,
|
|
71
|
+
` Invoke-RestMethod -Uri 'http://localhost:${agentPort}/api/shutdown' -Method POST ` +
|
|
72
|
+
`-Headers @{ Authorization = 'Bearer ${apiToken}' } -TimeoutSec 5 | Out-Null`,
|
|
73
|
+
` Log 'Graceful shutdown requested, waiting for offline heartbeat...'`,
|
|
74
|
+
` Start-Sleep -Seconds 4`,
|
|
75
|
+
` } catch {`,
|
|
76
|
+
` Log "Graceful shutdown API failed: $_ (will force kill)"`,
|
|
77
|
+
` }`,
|
|
78
|
+
].join('\n')
|
|
79
|
+
}
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Apply self-PID exclusion to a stopCmd so that the detached script
|
|
83
|
+
* does not kill itself when enumerating child processes.
|
|
84
|
+
*/
|
|
85
|
+
function makeSafeStopCmd(stopCmd) {
|
|
86
|
+
return stopCmd.replace(
|
|
67
87
|
/ForEach-Object\s*\{\s*Stop-Process/,
|
|
68
88
|
'Where-Object { $_.ProcessId -ne $PID } | ForEach-Object { Stop-Process'
|
|
69
89
|
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate a temporary PowerShell restart script that:
|
|
94
|
+
* 1. Requests graceful shutdown via HTTP API (sends offline heartbeat)
|
|
95
|
+
* 2. Force-kills remaining processes (fallback)
|
|
96
|
+
* 3. Restarts the agent
|
|
97
|
+
*
|
|
98
|
+
* @param {string} stopCmd - Command/script block to force-stop the agent
|
|
99
|
+
* @param {string} startCmd - Command/script block to start the agent
|
|
100
|
+
* @param {number} agentPort - The agent's HTTP port
|
|
101
|
+
* @param {string} apiToken - The agent's API token
|
|
102
|
+
* @returns {string} - Path to the generated restart script (.ps1)
|
|
103
|
+
*/
|
|
104
|
+
function buildRestartScript(stopCmd, startCmd, agentPort, apiToken) {
|
|
105
|
+
const dataDir = path.join(os.homedir(), '.minion')
|
|
106
|
+
const scriptPath = path.join(dataDir, 'restart-agent.ps1')
|
|
107
|
+
const logPath = path.join(dataDir, 'restart-agent.log')
|
|
108
|
+
|
|
109
|
+
const safeStopCmd = makeSafeStopCmd(stopCmd)
|
|
110
|
+
const gracefulStop = buildGracefulStopBlock(agentPort, apiToken)
|
|
111
|
+
|
|
112
|
+
const ps1 = [
|
|
113
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
114
|
+
`$logFile = '${logPath.replace(/\\/g, '\\\\')}'`,
|
|
115
|
+
`function Log($msg) { "$(Get-Date -Format o) $msg" | Out-File -Append $logFile }`,
|
|
116
|
+
`Log 'Restart started'`,
|
|
117
|
+
`try {`,
|
|
118
|
+
` Log 'Requesting graceful shutdown...'`,
|
|
119
|
+
gracefulStop,
|
|
120
|
+
` Log 'Force stopping remaining processes...'`,
|
|
121
|
+
` ${safeStopCmd}`,
|
|
122
|
+
` Start-Sleep -Seconds 2`,
|
|
123
|
+
` Log 'Starting agent...'`,
|
|
124
|
+
` ${startCmd}`,
|
|
125
|
+
` Log 'Restart completed successfully'`,
|
|
126
|
+
`} catch {`,
|
|
127
|
+
` Log "Restart failed: $_"`,
|
|
128
|
+
` Log 'Attempting to start agent anyway...'`,
|
|
129
|
+
` ${startCmd}`,
|
|
130
|
+
`}`,
|
|
131
|
+
].join('\n')
|
|
132
|
+
|
|
133
|
+
try { fs.mkdirSync(dataDir, { recursive: true }) } catch { /* exists */ }
|
|
134
|
+
fs.writeFileSync(scriptPath, ps1, 'utf-8')
|
|
135
|
+
|
|
136
|
+
return scriptPath
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate a temporary PowerShell script that:
|
|
141
|
+
* 1. Requests graceful shutdown via HTTP API (sends offline heartbeat)
|
|
142
|
+
* 2. Force-kills remaining processes (releases DLL file locks)
|
|
143
|
+
* 3. Runs npm install -g
|
|
144
|
+
* 4. Restarts the agent
|
|
145
|
+
*
|
|
146
|
+
* The force-kill after graceful shutdown is necessary because node-pty's
|
|
147
|
+
* conpty.node DLL is locked by the running process, causing EBUSY errors
|
|
148
|
+
* if npm tries to overwrite it in-place.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} npmInstallCmd - The npm install command to run
|
|
151
|
+
* @param {string} stopCmd - Command/script block to stop the agent
|
|
152
|
+
* @param {string} startCmd - Command/script block to start the agent
|
|
153
|
+
* @param {string} scriptName - Base name for the .ps1 and .log files
|
|
154
|
+
* @param {number} agentPort - The agent's HTTP port
|
|
155
|
+
* @param {string} apiToken - The agent's API token
|
|
156
|
+
* @returns {string} - Path to the generated update script (.ps1)
|
|
157
|
+
*/
|
|
158
|
+
function buildUpdateScript(npmInstallCmd, stopCmd, startCmd, scriptName = 'update-agent', agentPort = 8080, apiToken = '') {
|
|
159
|
+
const dataDir = path.join(os.homedir(), '.minion')
|
|
160
|
+
const scriptPath = path.join(dataDir, `${scriptName}.ps1`)
|
|
161
|
+
const logPath = path.join(dataDir, `${scriptName}.log`)
|
|
162
|
+
|
|
163
|
+
const safeStopCmd = makeSafeStopCmd(stopCmd)
|
|
164
|
+
const gracefulStop = buildGracefulStopBlock(agentPort, apiToken)
|
|
70
165
|
|
|
71
166
|
const ps1 = [
|
|
72
167
|
`$ErrorActionPreference = 'Stop'`,
|
|
@@ -74,7 +169,9 @@ function buildUpdateScript(npmInstallCmd, stopCmd, startCmd, scriptName = 'updat
|
|
|
74
169
|
`function Log($msg) { "$(Get-Date -Format o) $msg" | Out-File -Append $logFile }`,
|
|
75
170
|
`Log 'Update started'`,
|
|
76
171
|
`try {`,
|
|
77
|
-
` Log '
|
|
172
|
+
` Log 'Requesting graceful shutdown...'`,
|
|
173
|
+
gracefulStop,
|
|
174
|
+
` Log 'Force stopping remaining processes...'`,
|
|
78
175
|
` ${safeStopCmd}`,
|
|
79
176
|
` Start-Sleep -Seconds 3`,
|
|
80
177
|
` Log 'Installing package...'`,
|
|
@@ -103,22 +200,26 @@ function buildUpdateScript(npmInstallCmd, stopCmd, startCmd, scriptName = 'updat
|
|
|
103
200
|
/**
|
|
104
201
|
* Build allowed commands for the detected process manager.
|
|
105
202
|
* @param {string} procMgr - Process manager type
|
|
203
|
+
* @param {{ AGENT_PORT?: number, API_TOKEN?: string }} [agentConfig] - Agent config for graceful shutdown
|
|
106
204
|
* @returns {Record<string, { description: string; command: string; deferred?: boolean }>}
|
|
107
205
|
*/
|
|
108
|
-
function buildAllowedCommands(procMgr) {
|
|
206
|
+
function buildAllowedCommands(procMgr, agentConfig = {}) {
|
|
109
207
|
const commands = {}
|
|
208
|
+
const agentPort = agentConfig.AGENT_PORT || 8080
|
|
209
|
+
const apiToken = agentConfig.API_TOKEN || ''
|
|
110
210
|
|
|
111
211
|
if (procMgr === 'user-process') {
|
|
112
212
|
const dataDir = path.join(os.homedir(), '.minion')
|
|
113
213
|
const pidFile = path.join(dataDir, 'minion-agent.pid')
|
|
114
214
|
const startScript = path.join(dataDir, 'start-agent.ps1')
|
|
115
215
|
|
|
116
|
-
const stopBlock = `$
|
|
117
|
-
const startBlock = `Start-Process powershell -ArgumentList '-ExecutionPolicy Bypass -WindowStyle Hidden -File
|
|
216
|
+
const stopBlock = `$agentPid = Get-Content '${pidFile}' -ErrorAction SilentlyContinue; if ($agentPid) { Get-CimInstance Win32_Process -Filter ('ParentProcessId = ' + $agentPid) -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; Stop-Process -Id $agentPid -Force -ErrorAction SilentlyContinue }`
|
|
217
|
+
const startBlock = `Start-Process powershell -ArgumentList ('-ExecutionPolicy Bypass -WindowStyle Hidden -File \"' + '${startScript}' + '\"') -WindowStyle Hidden`
|
|
118
218
|
|
|
119
219
|
commands['restart-agent'] = {
|
|
120
220
|
description: 'Restart the minion agent process',
|
|
121
|
-
|
|
221
|
+
spawnArgs: ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
|
222
|
+
buildRestartScript(stopBlock, startBlock, agentPort, apiToken)]],
|
|
122
223
|
deferred: true,
|
|
123
224
|
}
|
|
124
225
|
commands['update-agent'] = {
|
|
@@ -127,6 +228,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
127
228
|
'npm install -g @geekbeer/minion@latest',
|
|
128
229
|
stopBlock,
|
|
129
230
|
startBlock,
|
|
231
|
+
'update-agent',
|
|
232
|
+
agentPort,
|
|
233
|
+
apiToken,
|
|
130
234
|
)]],
|
|
131
235
|
deferred: true,
|
|
132
236
|
}
|
|
@@ -137,14 +241,17 @@ function buildAllowedCommands(procMgr) {
|
|
|
137
241
|
stopBlock,
|
|
138
242
|
startBlock,
|
|
139
243
|
'update-agent-dev',
|
|
244
|
+
agentPort,
|
|
245
|
+
apiToken,
|
|
140
246
|
)]],
|
|
141
247
|
deferred: true,
|
|
142
248
|
}
|
|
143
249
|
commands['status-services'] = {
|
|
144
250
|
description: 'Show agent process info',
|
|
145
|
-
command: `powershell -Command "$pidFile = '${pidFile}'; if (Test-Path $pidFile) { $
|
|
251
|
+
command: `powershell -Command "$pidFile = '${pidFile}'; if (Test-Path $pidFile) { $agentPid = (Get-Content $pidFile -Raw).Trim(); $proc = Get-Process -Id $agentPid -ErrorAction SilentlyContinue; if ($proc) { Write-Host 'minion-agent: running (PID:' $agentPid ')' } else { Write-Host 'minion-agent: not running (stale PID)' } } else { Write-Host 'minion-agent: not running' }"`,
|
|
146
252
|
}
|
|
147
253
|
} else if (procMgr === 'nssm') {
|
|
254
|
+
// NSSM handles graceful stop via its own service control, so no HTTP API needed
|
|
148
255
|
commands['restart-agent'] = {
|
|
149
256
|
description: 'Restart the minion agent service',
|
|
150
257
|
command: 'nssm restart minion-agent',
|
|
@@ -156,6 +263,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
156
263
|
'npm install -g @geekbeer/minion@latest',
|
|
157
264
|
'nssm stop minion-agent',
|
|
158
265
|
'nssm start minion-agent',
|
|
266
|
+
'update-agent',
|
|
267
|
+
agentPort,
|
|
268
|
+
apiToken,
|
|
159
269
|
)]],
|
|
160
270
|
deferred: true,
|
|
161
271
|
}
|
|
@@ -166,6 +276,8 @@ function buildAllowedCommands(procMgr) {
|
|
|
166
276
|
'nssm stop minion-agent',
|
|
167
277
|
'nssm start minion-agent',
|
|
168
278
|
'update-agent-dev',
|
|
279
|
+
agentPort,
|
|
280
|
+
apiToken,
|
|
169
281
|
)]],
|
|
170
282
|
deferred: true,
|
|
171
283
|
}
|
|
@@ -178,6 +290,7 @@ function buildAllowedCommands(procMgr) {
|
|
|
178
290
|
command: 'nssm status minion-agent & nssm status minion-websockify',
|
|
179
291
|
}
|
|
180
292
|
} else if (procMgr === 'sc') {
|
|
293
|
+
// sc.exe handles graceful stop via service control, so no HTTP API needed
|
|
181
294
|
commands['restart-agent'] = {
|
|
182
295
|
description: 'Restart the minion agent service',
|
|
183
296
|
command: 'net stop minion-agent & net start minion-agent',
|
|
@@ -189,6 +302,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
189
302
|
'npm install -g @geekbeer/minion@latest',
|
|
190
303
|
'net stop minion-agent',
|
|
191
304
|
'net start minion-agent',
|
|
305
|
+
'update-agent',
|
|
306
|
+
agentPort,
|
|
307
|
+
apiToken,
|
|
192
308
|
)]],
|
|
193
309
|
deferred: true,
|
|
194
310
|
}
|
|
@@ -199,6 +315,8 @@ function buildAllowedCommands(procMgr) {
|
|
|
199
315
|
'net stop minion-agent',
|
|
200
316
|
'net start minion-agent',
|
|
201
317
|
'update-agent-dev',
|
|
318
|
+
agentPort,
|
|
319
|
+
apiToken,
|
|
202
320
|
)]],
|
|
203
321
|
deferred: true,
|
|
204
322
|
}
|
package/win/minion-cli.ps1
CHANGED
|
@@ -196,10 +196,27 @@ function Stop-MinionProcess {
|
|
|
196
196
|
$agentPid = (Get-Content $PidFile -Raw).Trim()
|
|
197
197
|
$proc = Get-Process -Id $agentPid -ErrorAction SilentlyContinue
|
|
198
198
|
if ($proc) {
|
|
199
|
-
#
|
|
199
|
+
# Graceful shutdown via HTTP API (sends offline heartbeat to HQ)
|
|
200
|
+
# This mirrors Linux's SIGTERM → shutdown() flow.
|
|
201
|
+
$token = ''
|
|
202
|
+
if (Test-Path $EnvFile) {
|
|
203
|
+
$envVars = Read-EnvFile $EnvFile
|
|
204
|
+
if ($envVars['API_TOKEN']) { $token = $envVars['API_TOKEN'] }
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
$headers = @{}
|
|
208
|
+
if ($token) { $headers['Authorization'] = "Bearer $token" }
|
|
209
|
+
Invoke-RestMethod -Uri "$AgentUrl/api/shutdown" -Method POST -Headers $headers -TimeoutSec 5 | Out-Null
|
|
210
|
+
Write-Host "Graceful shutdown requested, waiting..."
|
|
211
|
+
Start-Sleep -Seconds 4
|
|
212
|
+
} catch {
|
|
213
|
+
Write-Host "Graceful shutdown failed, force stopping..."
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Force kill remaining processes (node, websockify, cloudflared)
|
|
200
217
|
Get-CimInstance Win32_Process -Filter "ParentProcessId = $agentPid" -ErrorAction SilentlyContinue |
|
|
201
218
|
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
202
|
-
Stop-Process -Id $agentPid -Force
|
|
219
|
+
Stop-Process -Id $agentPid -Force -ErrorAction SilentlyContinue
|
|
203
220
|
Write-Host "minion-agent stopped (PID: $agentPid)"
|
|
204
221
|
} else {
|
|
205
222
|
Write-Host "minion-agent was not running (stale PID file)"
|
package/win/routes/commands.js
CHANGED
|
@@ -9,10 +9,11 @@ const { promisify } = require('util')
|
|
|
9
9
|
const execAsync = promisify(exec)
|
|
10
10
|
|
|
11
11
|
const { verifyToken } = require('../../core/lib/auth')
|
|
12
|
+
const { config } = require('../../core/config')
|
|
12
13
|
const { detectProcessManager, buildAllowedCommands } = require('../lib/process-manager')
|
|
13
14
|
|
|
14
15
|
const PROC_MGR = detectProcessManager()
|
|
15
|
-
const ALLOWED_COMMANDS = buildAllowedCommands(PROC_MGR)
|
|
16
|
+
const ALLOWED_COMMANDS = buildAllowedCommands(PROC_MGR, config)
|
|
16
17
|
|
|
17
18
|
async function commandRoutes(fastify) {
|
|
18
19
|
fastify.get('/api/commands', async (request, reply) => {
|
package/win/server.js
CHANGED
|
@@ -38,6 +38,7 @@ const revisionWatcher = require('../core/lib/revision-watcher')
|
|
|
38
38
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
39
39
|
const threadWatcher = require('../core/lib/thread-watcher')
|
|
40
40
|
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
|
|
41
|
+
const { verifyToken } = require('../core/lib/auth')
|
|
41
42
|
const { terminalRoutes, cleanupSessions } = require('./routes/terminal')
|
|
42
43
|
const { startTerminalServer, stopTerminalServer } = require('./terminal-server')
|
|
43
44
|
const { fileRoutes } = require('./routes/files')
|
|
@@ -214,6 +215,19 @@ async function registerRoutes(app) {
|
|
|
214
215
|
await app.register(helpThreadRoutes)
|
|
215
216
|
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
216
217
|
|
|
218
|
+
// Shutdown endpoint — allows detached restart/update scripts to trigger
|
|
219
|
+
// graceful shutdown (offline heartbeat) before force-killing the process.
|
|
220
|
+
// Windows has no SIGTERM equivalent for console processes, so this HTTP API
|
|
221
|
+
// serves the same role that SIGTERM plays on Linux.
|
|
222
|
+
app.post('/api/shutdown', async (request, reply) => {
|
|
223
|
+
if (!verifyToken(request)) {
|
|
224
|
+
reply.code(401)
|
|
225
|
+
return { success: false, error: 'Unauthorized' }
|
|
226
|
+
}
|
|
227
|
+
reply.send({ success: true })
|
|
228
|
+
setImmediate(() => shutdown('API'))
|
|
229
|
+
})
|
|
230
|
+
|
|
217
231
|
// Windows-specific routes
|
|
218
232
|
await app.register(commandRoutes)
|
|
219
233
|
await app.register(terminalRoutes)
|