@remcp/remcp 0.2.221 → 0.2.223
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcp/remcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.223",
|
|
4
4
|
"description": "ReMCP device client: pair a computer with ReMCP and run the outbound-only agent that hosts the local MCP runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-task-restart.mjs && node --check src/cli/windows-update-lock.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
|
|
21
|
+
"check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-owned-job-source.mjs && node --check src/cli/windows-owned-job.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-task-restart.mjs && node --check src/cli/windows-update-lock.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
|
|
22
22
|
"test": "node --test test/*.test.mjs"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Windows 10+: assign the child to an unnamed job during CreateProcess itself.
|
|
2
|
+
// https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute
|
|
3
|
+
// Only the helper holds the job handle. Neither child nor descendants inherit it.
|
|
4
|
+
export const windowsOwnedJobScript = String.raw`
|
|
5
|
+
$ErrorActionPreference = 'Stop'
|
|
6
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
7
|
+
$job = $null
|
|
8
|
+
try {
|
|
9
|
+
Add-Type -TypeDefinition @'
|
|
10
|
+
using System;
|
|
11
|
+
using System.ComponentModel;
|
|
12
|
+
using System.Diagnostics;
|
|
13
|
+
using System.Runtime.InteropServices;
|
|
14
|
+
using System.Text;
|
|
15
|
+
using System.Threading;
|
|
16
|
+
|
|
17
|
+
public sealed class ReMcpOwnedJob : IDisposable {
|
|
18
|
+
[StructLayout(LayoutKind.Sequential)] struct Limits {
|
|
19
|
+
public long ProcessTime, JobTime; public uint Flags;
|
|
20
|
+
public UIntPtr MinWorkingSet, MaxWorkingSet; public uint ActiveLimit;
|
|
21
|
+
public UIntPtr Affinity; public uint Priority, Scheduling;
|
|
22
|
+
}
|
|
23
|
+
[StructLayout(LayoutKind.Sequential)] struct Io {
|
|
24
|
+
public ulong ReadOps, WriteOps, OtherOps, ReadBytes, WriteBytes, OtherBytes;
|
|
25
|
+
}
|
|
26
|
+
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
|
|
27
|
+
public Limits Basic; public Io Counters;
|
|
28
|
+
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
|
|
29
|
+
}
|
|
30
|
+
[StructLayout(LayoutKind.Sequential)] struct Accounting {
|
|
31
|
+
public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
|
|
32
|
+
public uint PageFaults, TotalProcesses, ActiveProcesses, TerminatedProcesses;
|
|
33
|
+
}
|
|
34
|
+
[StructLayout(LayoutKind.Sequential)] struct Startup {
|
|
35
|
+
public uint Size; public IntPtr Reserved, Desktop, Title;
|
|
36
|
+
public uint X, Y, XSize, YSize, XChars, YChars, Fill, Flags;
|
|
37
|
+
public ushort Show, ReservedSize; public IntPtr ReservedData, In, Out, Error;
|
|
38
|
+
}
|
|
39
|
+
[StructLayout(LayoutKind.Sequential)] struct StartupEx {
|
|
40
|
+
public Startup Basic; public IntPtr Attributes;
|
|
41
|
+
}
|
|
42
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo {
|
|
43
|
+
public IntPtr Process, Thread; public uint Pid, Tid;
|
|
44
|
+
}
|
|
45
|
+
[StructLayout(LayoutKind.Sequential)] struct Security {
|
|
46
|
+
public int Size; public IntPtr Descriptor;
|
|
47
|
+
[MarshalAs(UnmanagedType.Bool)] public bool Inherit;
|
|
48
|
+
}
|
|
49
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr CreateJobObjectW(IntPtr security, IntPtr name);
|
|
50
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits limits, uint size);
|
|
51
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting info, uint size, IntPtr returned);
|
|
52
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool TerminateJobObject(IntPtr job, uint code);
|
|
53
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool InitializeProcThreadAttributeList(IntPtr list, int count, int flags, ref IntPtr size);
|
|
54
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool UpdateProcThreadAttribute(IntPtr list, uint flags, IntPtr key, IntPtr value, IntPtr size, IntPtr previous, IntPtr returned);
|
|
55
|
+
[DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
|
|
56
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool CreateProcessW(string application, StringBuilder command, IntPtr processSecurity, IntPtr threadSecurity, bool inherit, uint flags, IntPtr environment, string cwd, ref StartupEx startup, out ProcessInfo info);
|
|
57
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CreateFileW(string name, uint access, uint share, ref Security security, uint creation, uint flags, IntPtr template);
|
|
58
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
|
59
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool GetExitCodeProcess(IntPtr handle, out uint code);
|
|
60
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
61
|
+
IntPtr job, process;
|
|
62
|
+
int stop;
|
|
63
|
+
static void Check(bool ok) { if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error()); }
|
|
64
|
+
static void Close(IntPtr handle) { if (handle != IntPtr.Zero && handle != new IntPtr(-1)) CloseHandle(handle); }
|
|
65
|
+
public ReMcpOwnedJob() {
|
|
66
|
+
job = CreateJobObjectW(IntPtr.Zero, IntPtr.Zero);
|
|
67
|
+
if (job == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
68
|
+
try {
|
|
69
|
+
var limits = new ExtendedLimits();
|
|
70
|
+
limits.Basic.Flags = 0x2000; // KILL_ON_JOB_CLOSE; no breakaway allowed.
|
|
71
|
+
Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(limits)));
|
|
72
|
+
} catch { Dispose(); throw; }
|
|
73
|
+
}
|
|
74
|
+
public uint Start(string application, string command, string cwd, string log) {
|
|
75
|
+
var security = new Security { Size=Marshal.SizeOf(typeof(Security)), Inherit=true };
|
|
76
|
+
IntPtr output=IntPtr.Zero, input=IntPtr.Zero, list=IntPtr.Zero, handles=IntPtr.Zero, jobs=IntPtr.Zero;
|
|
77
|
+
bool initialized = false;
|
|
78
|
+
try {
|
|
79
|
+
output = CreateFileW(log, 4, 7, ref security, 4, 0x80, IntPtr.Zero); // append, OPEN_ALWAYS
|
|
80
|
+
Check(output != new IntPtr(-1));
|
|
81
|
+
input = CreateFileW("NUL", 0x80000000, 7, ref security, 3, 0x80, IntPtr.Zero);
|
|
82
|
+
Check(input != new IntPtr(-1));
|
|
83
|
+
IntPtr size = IntPtr.Zero;
|
|
84
|
+
InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref size);
|
|
85
|
+
Check(size != IntPtr.Zero);
|
|
86
|
+
list = Marshal.AllocHGlobal(size);
|
|
87
|
+
Check(InitializeProcThreadAttributeList(list, 2, 0, ref size)); initialized = true;
|
|
88
|
+
handles = Marshal.AllocHGlobal(IntPtr.Size * 2);
|
|
89
|
+
Marshal.WriteIntPtr(handles, input); Marshal.WriteIntPtr(handles, IntPtr.Size, output);
|
|
90
|
+
Check(UpdateProcThreadAttribute(list, 0, new IntPtr(0x20002), handles, new IntPtr(IntPtr.Size * 2), IntPtr.Zero, IntPtr.Zero));
|
|
91
|
+
jobs = Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(jobs, job);
|
|
92
|
+
Check(UpdateProcThreadAttribute(list, 0, new IntPtr(0x2000D), jobs, new IntPtr(IntPtr.Size), IntPtr.Zero, IntPtr.Zero));
|
|
93
|
+
var startup = new StartupEx();
|
|
94
|
+
startup.Basic.Size = (uint)Marshal.SizeOf(startup);
|
|
95
|
+
startup.Basic.Flags = 0x100; // STARTF_USESTDHANDLES
|
|
96
|
+
startup.Basic.In = input; startup.Basic.Out = output; startup.Basic.Error = output;
|
|
97
|
+
startup.Attributes = list;
|
|
98
|
+
ProcessInfo info;
|
|
99
|
+
// JOB_LIST attaches before execution, avoiding a suspended-but-unassigned orphan window.
|
|
100
|
+
Check(CreateProcessW(application, new StringBuilder(command), IntPtr.Zero, IntPtr.Zero,
|
|
101
|
+
true, 0x08080000, IntPtr.Zero, cwd, ref startup, out info));
|
|
102
|
+
process = info.Process; Close(info.Thread);
|
|
103
|
+
return info.Pid;
|
|
104
|
+
} finally {
|
|
105
|
+
if (initialized) DeleteProcThreadAttributeList(list);
|
|
106
|
+
if (list != IntPtr.Zero) Marshal.FreeHGlobal(list);
|
|
107
|
+
if (handles != IntPtr.Zero) Marshal.FreeHGlobal(handles);
|
|
108
|
+
if (jobs != IntPtr.Zero) Marshal.FreeHGlobal(jobs);
|
|
109
|
+
Close(input); Close(output);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
public uint Wait() {
|
|
113
|
+
var reader = new Thread(() => {
|
|
114
|
+
// EOF is ownership loss: only Node owns the write end of this private pipe.
|
|
115
|
+
try { Console.ReadLine(); } catch { } finally { Interlocked.Exchange(ref stop, 1); }
|
|
116
|
+
});
|
|
117
|
+
reader.IsBackground = true; reader.Start();
|
|
118
|
+
while (Volatile.Read(ref stop) == 0) {
|
|
119
|
+
uint state = WaitForSingleObject(process, 50);
|
|
120
|
+
if (state == 0) break;
|
|
121
|
+
if (state != 258) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
122
|
+
}
|
|
123
|
+
Check(TerminateJobObject(job, 1));
|
|
124
|
+
var timer = Stopwatch.StartNew();
|
|
125
|
+
while (true) {
|
|
126
|
+
Accounting info;
|
|
127
|
+
Check(QueryInformationJobObject(job, 1, out info, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero));
|
|
128
|
+
if (info.ActiveProcesses == 0) break;
|
|
129
|
+
if (timer.ElapsedMilliseconds >= 7000) throw new TimeoutException("Owned job did not become empty");
|
|
130
|
+
Thread.Sleep(20);
|
|
131
|
+
}
|
|
132
|
+
uint code; Check(GetExitCodeProcess(process, out code)); return code;
|
|
133
|
+
}
|
|
134
|
+
public void Dispose() { Close(job); job=IntPtr.Zero; Close(process); process=IntPtr.Zero; }
|
|
135
|
+
}
|
|
136
|
+
'@
|
|
137
|
+
$job = New-Object ReMcpOwnedJob
|
|
138
|
+
[Console]::WriteLine('{"ready":true}')
|
|
139
|
+
$line = [Console]::ReadLine()
|
|
140
|
+
if (!$line) { throw 'Job owner disconnected before start' }
|
|
141
|
+
$request = ConvertFrom-Json -InputObject ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($line)))
|
|
142
|
+
$childId = $job.Start($request.application, $request.command, $request.cwd, $request.log)
|
|
143
|
+
[Console]::WriteLine(('{"started":true,"pid":' + $childId + '}'))
|
|
144
|
+
$code = $job.Wait()
|
|
145
|
+
[Console]::WriteLine(('{"empty":true,"exitCode":' + $code + '}'))
|
|
146
|
+
} catch {
|
|
147
|
+
$cause = $_.Exception.GetBaseException()
|
|
148
|
+
[Console]::Error.WriteLine(('Windows owned job failed (' + $cause.GetType().Name + '; code=' + $cause.NativeErrorCode + ').'))
|
|
149
|
+
exit 1
|
|
150
|
+
} finally { if ($job) { $job.Dispose() } }
|
|
151
|
+
`;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { windowsOwnedJobScript } from './windows-owned-job-source.mjs';
|
|
4
|
+
|
|
5
|
+
function quote(value) {
|
|
6
|
+
if (typeof value !== 'string' || value.includes('\0')) throw new Error('Invalid owned-job argument');
|
|
7
|
+
return '"' + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1') + '"';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// The helper owns an unnamed kernel job; no PID enumeration or taskkill is used.
|
|
11
|
+
// Acknowledgement is emitted only after the job's ActiveProcesses count reaches zero.
|
|
12
|
+
export function spawnWindowsOwnedJob(application, args, { cwd, log, env = process.env } = {}) {
|
|
13
|
+
if (process.platform !== 'win32') throw new Error('Windows owned jobs require Windows');
|
|
14
|
+
if (![application, cwd, log].every(p => typeof p === 'string' && !p.includes('\0') && path.isAbsolute(p))) {
|
|
15
|
+
throw new Error('Owned-job paths must be absolute');
|
|
16
|
+
}
|
|
17
|
+
const command = [application, ...args].map(quote).join(' ');
|
|
18
|
+
if (command.length > 32766) throw new Error('Owned-job command line is too long');
|
|
19
|
+
const powershell = path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
20
|
+
const helper = spawn(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand',
|
|
21
|
+
Buffer.from(windowsOwnedJobScript, 'utf16le').toString('base64')], {
|
|
22
|
+
cwd:path.dirname(powershell), env, windowsHide:true, stdio:['pipe','pipe','pipe'],
|
|
23
|
+
});
|
|
24
|
+
let output = '', stderr = '', started = false, empty = false, settled = false;
|
|
25
|
+
let resolveReady, rejectReady, resolveDone, rejectDone;
|
|
26
|
+
const ready = new Promise((resolve, reject) => { resolveReady=resolve; rejectReady=reject; });
|
|
27
|
+
const done = new Promise((resolve, reject) => { resolveDone=resolve; rejectDone=reject; });
|
|
28
|
+
// Callers may observe readiness and completion separately; suppress unhandled rejection noise.
|
|
29
|
+
ready.catch(() => {}); done.catch(() => {});
|
|
30
|
+
const fail = error => { rejectReady(error); rejectDone(error); };
|
|
31
|
+
const timer = setTimeout(() => { fail(new Error('Windows job startup timed out')); helper.kill(); }, 15000);
|
|
32
|
+
helper.stdin.on('error', () => {});
|
|
33
|
+
helper.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-2048); });
|
|
34
|
+
helper.stdout.on('data', chunk => {
|
|
35
|
+
output += chunk.toString('utf8');
|
|
36
|
+
if (output.length > 4096) { fail(new Error('Invalid Windows job response')); helper.kill(); return; }
|
|
37
|
+
while (output.includes('\n')) {
|
|
38
|
+
const index = output.indexOf('\n'), line = output.slice(0,index).trim(); output=output.slice(index+1);
|
|
39
|
+
try {
|
|
40
|
+
const message = JSON.parse(line);
|
|
41
|
+
if (message.ready === true && !started) {
|
|
42
|
+
// ASCII envelope survives Windows PowerShell 5.1's OEM console codepage.
|
|
43
|
+
helper.stdin.write(Buffer.from(JSON.stringify({application,command,cwd,log}),'utf8').toString('base64') + '\n');
|
|
44
|
+
} else if (message.started === true && Number.isSafeInteger(message.pid) && message.pid > 0 && !started) {
|
|
45
|
+
started=true; helper.ownedJob.running=true; helper.ownedJob.pid=message.pid;
|
|
46
|
+
clearTimeout(timer); resolveReady(message);
|
|
47
|
+
} else if (message.empty === true && started) { empty=true; helper.ownedJob.running=false; }
|
|
48
|
+
else throw new Error('Unexpected Windows job response');
|
|
49
|
+
} catch { fail(new Error('Invalid Windows job response')); helper.kill(); }
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
helper.on('error', error => { clearTimeout(timer); fail(error); });
|
|
53
|
+
helper.on('close', code => {
|
|
54
|
+
clearTimeout(timer); helper.ownedJob.running=false; settled=true;
|
|
55
|
+
helper.ownedJob.confirmedEmpty = code === 0 && started && empty;
|
|
56
|
+
if (helper.ownedJob.confirmedEmpty) resolveDone();
|
|
57
|
+
else fail(new Error('Windows owned job failed: ' + stderr.trim().slice(0,300)));
|
|
58
|
+
if (!started) rejectReady(new Error('Windows job exited before readiness'));
|
|
59
|
+
});
|
|
60
|
+
helper.ownedJob = { ready, done, running:false, confirmedEmpty:false, pid:null, stop: async () => {
|
|
61
|
+
if (!settled) helper.stdin.end('STOP\n');
|
|
62
|
+
let timeout;
|
|
63
|
+
try { await Promise.race([done, new Promise((_, reject) => {
|
|
64
|
+
timeout=setTimeout(() => reject(new Error('Windows owned job stop timed out')), 9000);
|
|
65
|
+
})]); } finally { clearTimeout(timeout); }
|
|
66
|
+
}};
|
|
67
|
+
return helper;
|
|
68
|
+
}
|
|
@@ -67,11 +67,25 @@ export function withWindowsUpdateLock(prefix, operation) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const onExit = () => removeOwnedLock(directory, token);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let released = false;
|
|
71
|
+
const release = () => {
|
|
72
|
+
if (released) return;
|
|
73
|
+
released = true;
|
|
74
74
|
process.removeListener('exit', onExit);
|
|
75
75
|
removeOwnedLock(directory, token);
|
|
76
|
+
};
|
|
77
|
+
process.once('exit', onExit);
|
|
78
|
+
try {
|
|
79
|
+
const result = operation();
|
|
80
|
+
// A future async installer must retain the prefix lock until it settles.
|
|
81
|
+
// Releasing here would let another updater move the same package/shims.
|
|
82
|
+
if (result && typeof result.then === 'function') {
|
|
83
|
+
return Promise.resolve(result).finally(release);
|
|
84
|
+
}
|
|
85
|
+
release();
|
|
86
|
+
return result;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
release();
|
|
89
|
+
throw error;
|
|
76
90
|
}
|
|
77
91
|
}
|
|
@@ -8,6 +8,7 @@ import process from 'node:process';
|
|
|
8
8
|
import crypto from 'node:crypto';
|
|
9
9
|
import { spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { spawnWindowsOwnedJob } from './windows-owned-job.mjs';
|
|
11
12
|
|
|
12
13
|
const currentFile = fileURLToPath(import.meta.url);
|
|
13
14
|
const root = process.env.REMCP_CONFIG_DIR || path.join(os.homedir(), '.config', 'remcp');
|
|
@@ -94,33 +95,48 @@ async function serve(cliEntry) {
|
|
|
94
95
|
let requestedRestart = null;
|
|
95
96
|
let startedAt = 0;
|
|
96
97
|
let lastExit = null;
|
|
98
|
+
let containmentFailure = null;
|
|
97
99
|
const usedNonces = new Set();
|
|
98
100
|
|
|
99
101
|
function start() {
|
|
100
|
-
if (stopped || child) return;
|
|
102
|
+
if (stopped || child || containmentFailure) return;
|
|
101
103
|
const logFile = path.join(root, 'windows-startup-agent.log');
|
|
102
104
|
try { if (fs.statSync(logFile).size > 4 * 1024 * 1024) fs.truncateSync(logFile, 0); } catch {}
|
|
103
105
|
const log = fs.openSync(logFile, 'a', 0o600);
|
|
104
106
|
const prefix = path.resolve(path.dirname(cliEntry), '../../../../');
|
|
105
107
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const env = { ...process.env, REMCP_USER_STARTUP_SUPERVISOR:'1', NPM_CONFIG_PREFIX: prefix,
|
|
109
|
+
PATH: path.dirname(process.execPath) + path.delimiter + (process.env.PATH || '') };
|
|
110
|
+
child = process.platform === 'win32'
|
|
111
|
+
? spawnWindowsOwnedJob(process.execPath, [cliEntry, 'start', '--service'], {cwd:root,log:logFile,env})
|
|
112
|
+
: spawn(process.execPath, [cliEntry, 'start', '--service'], {
|
|
113
|
+
cwd:root, windowsHide:true, stdio:['ignore',log,log,'ipc'], env,
|
|
114
|
+
});
|
|
111
115
|
} finally { fs.closeSync(log); }
|
|
112
116
|
const ownChild = child;
|
|
113
117
|
startedAt = Date.now();
|
|
118
|
+
ownChild.ownedJob?.ready.then(() => { startedAt=Date.now(); }).catch(error => { lastExit=error.message; });
|
|
119
|
+
ownChild.ownedJob?.done.catch(error => { lastExit=error.message; });
|
|
114
120
|
ownChild.on('error', error => {
|
|
115
121
|
lastExit = error.message;
|
|
122
|
+
if (ownChild.ownedJob) {
|
|
123
|
+
containmentFailure = new Error('Windows job ownership failed; restart refused');
|
|
124
|
+
stopped=true;
|
|
125
|
+
return; // Retain the owner until close; only confirmedEmpty can release it.
|
|
126
|
+
}
|
|
116
127
|
if (child === ownChild) {
|
|
117
128
|
child = null;
|
|
118
129
|
if (!stopped) restartTimer = setTimeout(start, 3000);
|
|
119
130
|
}
|
|
120
131
|
});
|
|
121
|
-
ownChild.on('
|
|
132
|
+
ownChild.on('close', (code, signal) => {
|
|
122
133
|
if (child !== ownChild) return;
|
|
123
134
|
child = null;
|
|
135
|
+
// Completion failure does not prove the previous tree is empty. Fail closed.
|
|
136
|
+
if (ownChild.ownedJob && !ownChild.ownedJob.confirmedEmpty) {
|
|
137
|
+
containmentFailure = new Error('Windows job did not confirm an empty process tree; restart refused');
|
|
138
|
+
stopped=true; lastExit=containmentFailure.message; return;
|
|
139
|
+
}
|
|
124
140
|
lastExit = code === null ? String(signal || 'unknown') : String(code);
|
|
125
141
|
if (!stopped) restartTimer = setTimeout(start, 3000);
|
|
126
142
|
});
|
|
@@ -128,17 +144,12 @@ async function serve(cliEntry) {
|
|
|
128
144
|
|
|
129
145
|
async function endChild() {
|
|
130
146
|
clearTimeout(restartTimer);
|
|
147
|
+
if (containmentFailure) throw containmentFailure;
|
|
131
148
|
const ownChild = child;
|
|
132
149
|
if (!ownChild) return;
|
|
150
|
+
if (ownChild.ownedJob) { await ownChild.ownedJob.stop(); return; }
|
|
133
151
|
if (ownChild.exitCode === null && !ownChild.killed) {
|
|
134
|
-
|
|
135
|
-
const result = spawnSync('taskkill.exe', ['/PID', String(ownChild.pid), '/T', '/F'], {
|
|
136
|
-
windowsHide: true, encoding: 'utf8', timeout: 7000, maxBuffer: 2048,
|
|
137
|
-
});
|
|
138
|
-
if (result.status !== 0 && ownChild.exitCode === null) {
|
|
139
|
-
throw new Error('Could not terminate owned agent process tree');
|
|
140
|
-
}
|
|
141
|
-
} else ownChild.kill('SIGTERM');
|
|
152
|
+
ownChild.kill('SIGTERM');
|
|
142
153
|
}
|
|
143
154
|
if (ownChild.exitCode === null) await Promise.race([
|
|
144
155
|
new Promise(resolve => ownChild.once('exit', resolve)),
|
|
@@ -184,6 +195,7 @@ async function serve(cliEntry) {
|
|
|
184
195
|
process.exit(0);
|
|
185
196
|
});
|
|
186
197
|
} else if (request.action === 'restart') {
|
|
198
|
+
if (containmentFailure) throw containmentFailure;
|
|
187
199
|
// Agent-initiated npm updates call this from a child of the current agent.
|
|
188
200
|
// Killing that tree before the update CLI returns can abort the updater itself.
|
|
189
201
|
// Coalesce requests and schedule the owned-child handoff after acknowledgement.
|
|
@@ -194,14 +206,14 @@ async function serve(cliEntry) {
|
|
|
194
206
|
stopped = true;
|
|
195
207
|
try { await endChild(); }
|
|
196
208
|
catch (error) { console.error('ReMCP supervisor restart failed:', error.message); }
|
|
197
|
-
finally { stopped = false; if (!child) start(); }
|
|
209
|
+
finally { if (!containmentFailure) { stopped = false; if (!child) start(); } }
|
|
198
210
|
})();
|
|
199
211
|
}, 2500);
|
|
200
212
|
}
|
|
201
213
|
reply({ ok: true, restartScheduled: true });
|
|
202
214
|
} else reply({
|
|
203
|
-
ok: true, childPid: child?.pid || null,
|
|
204
|
-
running: Boolean(child?.pid && child.exitCode === null),
|
|
215
|
+
ok: true, childPid: child?.ownedJob?.pid || child?.pid || null,
|
|
216
|
+
running: Boolean(child?.pid && child.exitCode === null && (!child.ownedJob || child.ownedJob.running)),
|
|
205
217
|
restartPending: Boolean(requestedRestart),
|
|
206
218
|
uptimeMs: child ? Date.now() - startedAt : null, lastExit,
|
|
207
219
|
});
|