@geekbeer/minion 2.67.0 → 2.68.0
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 +134 -13
- 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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
80
|
+
|
|
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) {
|
|
|
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,10 +200,13 @@ function buildUpdateScript(npmInstallCmd, stopCmd, startCmd) {
|
|
|
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')
|
|
@@ -118,7 +218,8 @@ function buildAllowedCommands(procMgr) {
|
|
|
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
|
}
|
|
@@ -136,6 +240,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
136
240
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
137
241
|
stopBlock,
|
|
138
242
|
startBlock,
|
|
243
|
+
'update-agent-dev',
|
|
244
|
+
agentPort,
|
|
245
|
+
apiToken,
|
|
139
246
|
)]],
|
|
140
247
|
deferred: true,
|
|
141
248
|
}
|
|
@@ -144,6 +251,7 @@ function buildAllowedCommands(procMgr) {
|
|
|
144
251
|
command: `powershell -Command "$pidFile = '${pidFile}'; if (Test-Path $pidFile) { $pid = (Get-Content $pidFile -Raw).Trim(); $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue; if ($proc) { Write-Host 'minion-agent: running (PID:' $pid ')' } else { Write-Host 'minion-agent: not running (stale PID)' } } else { Write-Host 'minion-agent: not running' }"`,
|
|
145
252
|
}
|
|
146
253
|
} else if (procMgr === 'nssm') {
|
|
254
|
+
// NSSM handles graceful stop via its own service control, so no HTTP API needed
|
|
147
255
|
commands['restart-agent'] = {
|
|
148
256
|
description: 'Restart the minion agent service',
|
|
149
257
|
command: 'nssm restart minion-agent',
|
|
@@ -155,6 +263,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
155
263
|
'npm install -g @geekbeer/minion@latest',
|
|
156
264
|
'nssm stop minion-agent',
|
|
157
265
|
'nssm start minion-agent',
|
|
266
|
+
'update-agent',
|
|
267
|
+
agentPort,
|
|
268
|
+
apiToken,
|
|
158
269
|
)]],
|
|
159
270
|
deferred: true,
|
|
160
271
|
}
|
|
@@ -164,6 +275,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
164
275
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
165
276
|
'nssm stop minion-agent',
|
|
166
277
|
'nssm start minion-agent',
|
|
278
|
+
'update-agent-dev',
|
|
279
|
+
agentPort,
|
|
280
|
+
apiToken,
|
|
167
281
|
)]],
|
|
168
282
|
deferred: true,
|
|
169
283
|
}
|
|
@@ -176,6 +290,7 @@ function buildAllowedCommands(procMgr) {
|
|
|
176
290
|
command: 'nssm status minion-agent & nssm status minion-websockify',
|
|
177
291
|
}
|
|
178
292
|
} else if (procMgr === 'sc') {
|
|
293
|
+
// sc.exe handles graceful stop via service control, so no HTTP API needed
|
|
179
294
|
commands['restart-agent'] = {
|
|
180
295
|
description: 'Restart the minion agent service',
|
|
181
296
|
command: 'net stop minion-agent & net start minion-agent',
|
|
@@ -187,6 +302,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
187
302
|
'npm install -g @geekbeer/minion@latest',
|
|
188
303
|
'net stop minion-agent',
|
|
189
304
|
'net start minion-agent',
|
|
305
|
+
'update-agent',
|
|
306
|
+
agentPort,
|
|
307
|
+
apiToken,
|
|
190
308
|
)]],
|
|
191
309
|
deferred: true,
|
|
192
310
|
}
|
|
@@ -196,6 +314,9 @@ function buildAllowedCommands(procMgr) {
|
|
|
196
314
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
197
315
|
'net stop minion-agent',
|
|
198
316
|
'net start minion-agent',
|
|
317
|
+
'update-agent-dev',
|
|
318
|
+
agentPort,
|
|
319
|
+
apiToken,
|
|
199
320
|
)]],
|
|
200
321
|
deferred: true,
|
|
201
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)
|