@mindexec/cli 0.2.140 → 0.2.142
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
CHANGED
|
@@ -124,6 +124,30 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, label }) {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
async function startHangingRemoteManager(port) {
|
|
128
|
+
const sockets = new Set();
|
|
129
|
+
const server = net.createServer(socket => {
|
|
130
|
+
sockets.add(socket);
|
|
131
|
+
socket.on('close', () => sockets.delete(socket));
|
|
132
|
+
socket.on('error', () => sockets.delete(socket));
|
|
133
|
+
});
|
|
134
|
+
server.unref();
|
|
135
|
+
await new Promise((resolve, reject) => {
|
|
136
|
+
server.once('error', reject);
|
|
137
|
+
server.listen(port, '127.0.0.1', resolve);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
endpoint: `127.0.0.1:${port}`,
|
|
142
|
+
async stop() {
|
|
143
|
+
for (const socket of sockets) {
|
|
144
|
+
socket.destroy();
|
|
145
|
+
}
|
|
146
|
+
await new Promise(resolve => server.close(resolve));
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
async function waitForBridge(bridge) {
|
|
128
152
|
return await waitFor(async () => {
|
|
129
153
|
const result = await fetchJson(`${bridge.baseUrl}/api/status`);
|
|
@@ -159,11 +183,13 @@ async function main() {
|
|
|
159
183
|
const clientRemotePort = await findFreePort();
|
|
160
184
|
const stalePort = await findFreePort();
|
|
161
185
|
const managerEndpoint = `127.0.0.1:${hostRemotePort}`;
|
|
186
|
+
let hangingManager = null;
|
|
162
187
|
const staleEndpoint = `127.0.0.1:${stalePort}`;
|
|
163
188
|
|
|
164
189
|
let hostBridge = null;
|
|
165
190
|
let clientBridge = null;
|
|
166
191
|
try {
|
|
192
|
+
hangingManager = await startHangingRemoteManager(stalePort);
|
|
167
193
|
hostBridge = spawnBridge({
|
|
168
194
|
bridgePort: hostBridgePort,
|
|
169
195
|
remoteHubPort: hostRemotePort,
|
|
@@ -180,11 +206,16 @@ async function main() {
|
|
|
180
206
|
});
|
|
181
207
|
await waitForBridge(clientBridge);
|
|
182
208
|
|
|
209
|
+
const firstConnectStartedAt = Date.now();
|
|
183
210
|
const firstAgent = await connectManagedAgent(clientBridge, managerEndpoint, staleEndpoint);
|
|
211
|
+
const firstConnectMs = Date.now() - firstConnectStartedAt;
|
|
184
212
|
assert.equal(firstAgent.running, true, JSON.stringify(firstAgent));
|
|
185
213
|
assert.equal(firstAgent.ready, true, JSON.stringify(firstAgent));
|
|
186
214
|
assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
|
|
187
215
|
assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
|
|
216
|
+
assert.ok(
|
|
217
|
+
firstConnectMs < 4500,
|
|
218
|
+
`managed RemoteAgent must race slow endpoint candidates instead of waiting sequentially, got ${firstConnectMs}ms`);
|
|
188
219
|
|
|
189
220
|
const connectedDevice = await waitFor(async () => {
|
|
190
221
|
const result = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`);
|
|
@@ -218,6 +249,9 @@ async function main() {
|
|
|
218
249
|
|
|
219
250
|
console.log('RemoteAgent managed supervisor smoke OK');
|
|
220
251
|
} finally {
|
|
252
|
+
if (hangingManager) {
|
|
253
|
+
await hangingManager.stop();
|
|
254
|
+
}
|
|
221
255
|
if (clientBridge) {
|
|
222
256
|
await clientBridge.stop();
|
|
223
257
|
}
|
|
@@ -317,6 +317,20 @@ async function waitForManagedAgent(bridge, managerEndpoint, label, timeoutMs = 2
|
|
|
317
317
|
}, timeoutMs, `${label} managed agent\n${bridge.details()}`);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
async function waitForManagedAgentRestart(bridge, managerEndpoint, previousPid, label, timeoutMs = 20000) {
|
|
321
|
+
return await waitFor(async () => {
|
|
322
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/remote/agent/status`);
|
|
323
|
+
const agent = result.payload;
|
|
324
|
+
return agent?.running === true
|
|
325
|
+
&& agent?.ready === true
|
|
326
|
+
&& String(agent.manager || '') === managerEndpoint
|
|
327
|
+
&& Number(agent.pid || 0) > 0
|
|
328
|
+
&& Number(agent.pid || 0) !== Number(previousPid || 0)
|
|
329
|
+
? agent
|
|
330
|
+
: null;
|
|
331
|
+
}, timeoutMs, `${label} managed agent restart\n${bridge.details()}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
320
334
|
async function waitForConnectedDevice(bridge, label) {
|
|
321
335
|
return await waitFor(async () => {
|
|
322
336
|
const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
|
|
@@ -324,6 +338,18 @@ async function waitForConnectedDevice(bridge, label) {
|
|
|
324
338
|
}, 12000, `${label} connected device\n${bridge.details()}`);
|
|
325
339
|
}
|
|
326
340
|
|
|
341
|
+
function killProcess(pid) {
|
|
342
|
+
const value = Number(pid || 0);
|
|
343
|
+
assert.ok(value > 0, `expected positive pid, got ${pid}`);
|
|
344
|
+
try {
|
|
345
|
+
process.kill(value, 'SIGTERM');
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (error?.code !== 'ESRCH') {
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
327
353
|
async function main() {
|
|
328
354
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
|
|
329
355
|
let fakeSupabase = null;
|
|
@@ -407,6 +433,14 @@ async function main() {
|
|
|
407
433
|
assert.equal(secondAgent.leaseId, 'lease-b');
|
|
408
434
|
await waitForConnectedDevice(hostB, 'host-b');
|
|
409
435
|
|
|
436
|
+
const secondPid = Number(secondAgent.pid || 0);
|
|
437
|
+
killProcess(secondPid);
|
|
438
|
+
const restartedAgent = await waitForManagedAgentRestart(client, hostBEndpoint, secondPid, 'host-b', 12000);
|
|
439
|
+
assert.equal(restartedAgent.usingNpx, false, JSON.stringify(restartedAgent));
|
|
440
|
+
assert.match(String(restartedAgent.launcher || ''), /mindexec-remote-fast/i);
|
|
441
|
+
assert.equal(restartedAgent.leaseId, 'lease-b');
|
|
442
|
+
await waitForConnectedDevice(hostB, 'host-b after restart');
|
|
443
|
+
|
|
410
444
|
currentTarget = {
|
|
411
445
|
...currentTarget,
|
|
412
446
|
active: false,
|
package/server.js
CHANGED
|
@@ -3132,7 +3132,7 @@ const REMOTE_AGENT_EARLY_EXIT_MS = 1200;
|
|
|
3132
3132
|
const REMOTE_AGENT_READY_TIMEOUT_MS = 5000;
|
|
3133
3133
|
const REMOTE_AGENT_DEFAULT_ENGINE = 'auto';
|
|
3134
3134
|
const REMOTE_AGENT_SYNC_REPORT_LOG_REPEAT_MS = 30000;
|
|
3135
|
-
const REMOTE_AGENT_RACE_START_STAGGER_MS =
|
|
3135
|
+
const REMOTE_AGENT_RACE_START_STAGGER_MS = Math.max(0, Number(process.env.MINDEXEC_REMOTE_AGENT_RACE_STAGGER_MS ?? 0) || 0);
|
|
3136
3136
|
const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
|
|
3137
3137
|
const REMOTE_AGENT_RECENT_MANAGER_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
3138
3138
|
const REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT = 64;
|
|
@@ -3151,6 +3151,7 @@ let remoteAgentSyncReportState = null;
|
|
|
3151
3151
|
let remoteAgentSyncReportLogKey = '';
|
|
3152
3152
|
let remoteAgentSyncReportLogAt = 0;
|
|
3153
3153
|
let remoteAgentConnectPromise = null;
|
|
3154
|
+
const remoteAgentIntentionalStopKeys = new Set();
|
|
3154
3155
|
const remoteAgentRecentSuccessfulManagers = new Map();
|
|
3155
3156
|
let remoteAgentRecentSuccessfulManagersLoaded = false;
|
|
3156
3157
|
let remoteAgentRecentSuccessfulManagersSavePromise = null;
|
|
@@ -3656,6 +3657,36 @@ function isRemoteAgentRegistryOwned() {
|
|
|
3656
3657
|
&& /registry/i.test(String(remoteAgentState.source || ''));
|
|
3657
3658
|
}
|
|
3658
3659
|
|
|
3660
|
+
function isRemoteAgentRegistryState(state) {
|
|
3661
|
+
return /registry/i.test(String(state?.source || ''));
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
function suppressRemoteAgentAutoRetry(connectionKey) {
|
|
3665
|
+
const key = safeRemoteAgentField(connectionKey, 128);
|
|
3666
|
+
if (!key) {
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
remoteAgentIntentionalStopKeys.add(key);
|
|
3671
|
+
const timer = setTimeout(() => {
|
|
3672
|
+
remoteAgentIntentionalStopKeys.delete(key);
|
|
3673
|
+
}, 10000);
|
|
3674
|
+
timer?.unref?.();
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
function maybeRetryRemoteAgentAfterExit(state, reason = 'agent-exited') {
|
|
3678
|
+
if (!state
|
|
3679
|
+
|| !REMOTE_REGISTRY_FOLLOWER_ENABLED
|
|
3680
|
+
|| isShuttingDown
|
|
3681
|
+
|| !isRemoteAgentRegistryState(state)
|
|
3682
|
+
|| remoteAgentIntentionalStopKeys.has(String(state.connectionKey || ''))) {
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
logWarn('remote', `managed RemoteAgent registry process ended; waking registry follower ${formatKeyValue('reason', reason)} ${formatKeyValue('manager', state.manager || '-')}`);
|
|
3687
|
+
scheduleRemoteRegistryFollower(0, reason);
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3659
3690
|
function isLocalRemoteHostTargetActive() {
|
|
3660
3691
|
const status = remoteHub.getStatus({ includeSecrets: false });
|
|
3661
3692
|
return status?.hostTargetActive === true
|
|
@@ -3912,6 +3943,7 @@ function resolveNpxCliLauncher() {
|
|
|
3912
3943
|
|
|
3913
3944
|
async function stopRemoteAgentConnection(reason = 'stopped') {
|
|
3914
3945
|
const previous = remoteAgentState;
|
|
3946
|
+
suppressRemoteAgentAutoRetry(previous.connectionKey);
|
|
3915
3947
|
if (previous.proc) {
|
|
3916
3948
|
try {
|
|
3917
3949
|
await terminateProcessTree(previous.proc);
|
|
@@ -4143,6 +4175,17 @@ function createRemoteAgentAttempt(options = {}) {
|
|
|
4143
4175
|
state.lastError = signal ? `signal:${signal}` : `exit:${code}`;
|
|
4144
4176
|
}
|
|
4145
4177
|
attempt.error = attempt.error || state.lastError || `exit:${code ?? 'unknown'}`;
|
|
4178
|
+
if (remoteAgentState === state
|
|
4179
|
+
&& remoteAgentState.connectionKey === state.connectionKey) {
|
|
4180
|
+
const serialized = serializeRemoteAgentState();
|
|
4181
|
+
if (state.status === 'failed') {
|
|
4182
|
+
logWarn('remote', `managed RemoteAgent failed: ${formatRemoteAgentFailureSummary(state)}`);
|
|
4183
|
+
} else {
|
|
4184
|
+
logEvent('remote', `managed RemoteAgent exited ${formatKeyValue('code', code ?? 0)}`, 'remote');
|
|
4185
|
+
}
|
|
4186
|
+
emitBridgeEvent(state.status === 'exited' ? 'RemoteAgentExited' : 'RemoteAgentFailed', serialized);
|
|
4187
|
+
maybeRetryRemoteAgentAfterExit(state, state.status === 'exited' ? 'agent-exited' : 'agent-failed');
|
|
4188
|
+
}
|
|
4146
4189
|
});
|
|
4147
4190
|
} catch (err) {
|
|
4148
4191
|
attempt.failed = true;
|