omni-notify-mcp 1.3.303 → 1.3.304

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.
@@ -171,23 +171,8 @@ export function ownerBoundLaunch(invocation, ownerPid, cwd, guardDir) {
171
171
  };
172
172
  }
173
173
  return {
174
- cmd: "powershell.exe",
175
- args: [
176
- "-NoProfile",
177
- "-NonInteractive",
178
- "-ExecutionPolicy",
179
- "Bypass",
180
- "-File",
181
- path.join(guardDir, "ownerGuard.ps1"),
182
- "-OwnerPid",
183
- String(ownerPid),
184
- "-ExeB64",
185
- encoded(invocation.cmd),
186
- "-ArgsB64",
187
- encoded(JSON.stringify(invocation.args)),
188
- "-CwdB64",
189
- encoded(cwd),
190
- ],
174
+ cmd: process.execPath,
175
+ args: [path.join(guardDir, "ownerGuard.js"), String(ownerPid), encoded(invocation.cmd), encoded(JSON.stringify(invocation.args)), encoded(cwd)],
191
176
  };
192
177
  }
193
178
  // GATE: how long a CLI tree gets to exit on SIGTERM before it is SIGKILLed — POSIX only; Windows stops it at once
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-notify-mcp",
3
- "version": "1.3.303",
3
+ "version": "1.3.304",
4
4
  "description": "Reach your AI agents from anywhere — and let them reach you. One stdio process serves the whole tool surface to Claude Code, GitHub Copilot Chat and Codex: desktop, Telegram, Slack, ntfy, Discord, Teams and SMS are configured in one file, and agents notify you when long work finishes or a decision is needed, ask you a question and wait for your answer, and receive and reply to the messages you send them — with Do Not Disturb and idle detection deciding what actually gets through.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ // OWNER-BOUND LAUNCH, Windows, in Node (#3786; run by process.execPath, never a PATH lookup): start one target and end it, with everything it started, the moment its owner process is gone.
3
+ // argv: <ownerPid> <exeB64> <argsB64 (JSON array)> <cwdB64>. Exit: the target's own exit code; 95 when the owner was already gone, 96 when the
4
+ // target could not start, 93 when its folder does not exist, 97 when a .cmd target is handed a line break (cmd.exe cannot carry one). Node cannot create a Windows job object, so the tree is ended by `taskkill /T` from the target's pid while it lives.
5
+ const { spawn, spawnSync } = require("node:child_process");
6
+ const fs = require("node:fs");
7
+
8
+ const [ownerArg, exeB64, argsB64, cwdB64] = process.argv.slice(2);
9
+ const decode = (text) => Buffer.from(text, "base64").toString("utf8");
10
+ const owner = Number(ownerArg);
11
+
12
+ const alive = (pid) => {
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ } catch (cause) {
17
+ return cause.code === "EPERM";
18
+ }
19
+ };
20
+
21
+ if (!alive(owner)) {
22
+ console.error(`ownerGuard: the owner process ${owner} is gone - nothing was started`);
23
+ process.exit(95);
24
+ }
25
+
26
+ // A .cmd/.bat target runs through cmd.exe, which reads the line once: every argument is wholly quoted, a quote doubled and a percent sign stepped out of the quotes behind a caret, so it arrives as plain text.
27
+ const caret = (text) => `"${text.replace(/(\\*)"/g, '$1$1""').replace(/(\\+)$/, "$1$1").replace(/%/g, '"^%"')}"`;
28
+
29
+ // The guard itself runs as Node through ELECTRON_RUN_AS_NODE; the target must not inherit it.
30
+ const { ELECTRON_RUN_AS_NODE: _runAsNode, ...targetEnv } = process.env;
31
+ const exe = decode(exeB64);
32
+ const args = JSON.parse(decode(argsB64));
33
+ const cwd = decode(cwdB64);
34
+ if (cwd !== "" && !fs.existsSync(cwd)) {
35
+ console.error(`ownerGuard: the folder ${cwd} does not exist - nothing was started`);
36
+ process.exit(93);
37
+ }
38
+ const viaCmd = /\.(cmd|bat)$/i.test(exe);
39
+ if (viaCmd && args.some((arg) => /[\r\n]/.test(arg))) {
40
+ console.error("ownerGuard: a .cmd target cannot carry a line break in an argument - nothing was started");
41
+ process.exit(97);
42
+ }
43
+ const target = viaCmd
44
+ ? spawn(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${caret(exe)} ${args.map((arg) => caret(arg)).join(" ")}"`], { cwd: cwd === "" ? undefined : cwd, stdio: "inherit", env: targetEnv, windowsHide: true, windowsVerbatimArguments: true })
45
+ : spawn(exe, args, { cwd: cwd === "" ? undefined : cwd, stdio: "inherit", env: targetEnv, windowsHide: true });
46
+ target.on("error", (cause) => {
47
+ console.error(`ownerGuard: the target could not start - ${cause.message}`);
48
+ process.exit(96);
49
+ });
50
+
51
+ const endTree = () => {
52
+ if (target.pid !== undefined && target.exitCode === null) spawnSync("taskkill", ["/PID", String(target.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
53
+ };
54
+
55
+ // GATE: how often the owner is checked — at the boundary the target outlives a dead owner by up to this long
56
+ const watch = setInterval(() => {
57
+ if (!alive(owner)) {
58
+ endTree();
59
+ process.exit(95);
60
+ }
61
+ }, 500);
62
+
63
+ target.on("exit", (code, signal) => {
64
+ clearInterval(watch);
65
+ process.exit(code ?? (signal === null ? 1 : 128));
66
+ });
@@ -1,665 +0,0 @@
1
- // OWNER-BOUND JOB (#1968, #1983): the ONE source of the kill-on-close Job Object that ends a process tree
2
- // with its owner. Loaded by ownerGuard.ps1 through Add-Type and called as OwnerJob.Run(...) for FleetView's
3
- // own launches.
4
- //
5
- // #2925: it is NO LONGER compiled into ~/.bullseye/bin/ownerGuard.exe for `claudeCode.claudeProcessWrapper`.
6
- // That build ran the .NET Framework's csc.exe at every activation, which made a compiler a dependency of a
7
- // marketplace install; the wrapper is now POSIX-only (media/shared/claudeWrapper.sh, a shell script that needs
8
- // nothing but /bin/sh) and Windows installs none. Nothing in this file may be re-wired into that setting.
9
- // Language level is C# 5 (the csc.exe that ships with Windows).
10
- //
11
- // THE MECHANISM: the target is created SUSPENDED, assigned to a job carrying KILL_ON_JOB_CLOSE, and only then
12
- // resumed, so no child can be born outside the job. The guard waits on the owner and the target and, when
13
- // EITHER ends, terminates the job. BREAKAWAY_OK is set on purpose and SILENT_BREAKAWAY is not: a process that
14
- // asks for CREATE_BREAKAWAY_FROM_JOB may leave, but nothing leaves by accident - except under `contain`, which leaves
15
- // BREAKAWAY_OK off because the msys2 runtime asks to break away from every job that allows it (spawn.cc), so a
16
- // Git Bash tree would otherwise leave the job whole. A child that asks then stays (Microsoft, "Nested Jobs").
17
- //
18
- // usage (wrapper mode, owner = the parent process, which is the extension host): ownerGuard.exe <exe> [args...]
19
- // (explicit owner): ownerGuard.exe --owner <pid> <exe> [args...]
20
- using System;
21
- using System.Collections.Generic;
22
- using System.IO;
23
- using System.Runtime.InteropServices;
24
- using System.Text;
25
- using System.Text.RegularExpressions;
26
- using System.Threading;
27
-
28
- public static class OwnerJob
29
- {
30
- const uint KILL_ON_JOB_CLOSE = 0x2000;
31
- const uint BREAKAWAY_OK = 0x800;
32
- const int EXTENDED_LIMIT_INFORMATION = 9;
33
- const uint CREATE_SUSPENDED = 0x00000004;
34
- const int STARTF_USESTDHANDLES = 0x00000100;
35
- const uint SYNCHRONIZE = 0x00100000;
36
- const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
37
- const uint INFINITE = 0xFFFFFFFF;
38
-
39
- public const int EXIT_NO_TARGET = 90;
40
- public const int EXIT_NO_JOB = 91;
41
- public const int EXIT_NO_LIMIT = 92;
42
- public const int EXIT_NOT_CREATED = 93;
43
- public const int EXIT_NOT_ASSIGNED = 94;
44
- public const int EXIT_OWNER_GONE = 95;
45
- public const int EXIT_NOT_FOUND = 96;
46
- public const int EXIT_BATCH_LINE_BREAK = 97;
47
- public const int EXIT_NOT_RESUMED = 98;
48
- public const int EXIT_WAIT_FAILED = 99;
49
- public const int EXIT_NO_EXIT_CODE = 100;
50
- public const int EXIT_DEADLINE = 101;
51
- public const int EXIT_ALREADY_RUNNING = 102;
52
- public const int FRAME_OWNERS = -1;
53
- const uint WAIT_TIMEOUT = 0x102;
54
- const uint STILL_ACTIVE = 259;
55
- const int ERROR_INVALID_PARAMETER = 87;
56
-
57
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
58
- struct STARTUPINFO
59
- {
60
- public int cb;
61
- public string lpReserved;
62
- public string lpDesktop;
63
- public string lpTitle;
64
- public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags;
65
- public short wShowWindow, cbReserved2;
66
- public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
67
- }
68
-
69
- [StructLayout(LayoutKind.Sequential)]
70
- struct PROCESS_INFORMATION
71
- {
72
- public IntPtr hProcess, hThread;
73
- public int dwProcessId, dwThreadId;
74
- }
75
-
76
- [StructLayout(LayoutKind.Sequential)]
77
- struct IO_COUNTERS
78
- {
79
- public ulong a, b, c, d, e, f;
80
- }
81
-
82
- [StructLayout(LayoutKind.Sequential)]
83
- struct JOBOBJECT_BASIC_LIMIT_INFORMATION
84
- {
85
- public long PerProcessUserTimeLimit, PerJobUserTimeLimit;
86
- public uint LimitFlags;
87
- public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize;
88
- public uint ActiveProcessLimit;
89
- public UIntPtr Affinity;
90
- public uint PriorityClass, SchedulingClass;
91
- }
92
-
93
- [StructLayout(LayoutKind.Sequential)]
94
- struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
95
- {
96
- public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
97
- public IO_COUNTERS IoInfo;
98
- public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
99
- }
100
-
101
- [StructLayout(LayoutKind.Sequential)]
102
- struct PROCESS_BASIC_INFORMATION
103
- {
104
- public IntPtr ExitStatus, PebBaseAddress, AffinityMask, BasePriority, UniqueProcessId, InheritedFromUniqueProcessId;
105
- }
106
-
107
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
108
- static extern IntPtr CreateJobObject(IntPtr attributes, string name);
109
- [DllImport("kernel32.dll", SetLastError = true)]
110
- static extern bool SetInformationJobObject(IntPtr job, int infoClass, IntPtr info, uint length);
111
- [DllImport("kernel32.dll", SetLastError = true)]
112
- static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
113
- [DllImport("kernel32.dll", SetLastError = true)]
114
- static extern bool TerminateJobObject(IntPtr job, uint exitCode);
115
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
116
- static extern bool CreateProcess(string application, string commandLine, IntPtr processAttributes, IntPtr threadAttributes,
117
- bool inheritHandles, uint creationFlags, IntPtr environment, string currentDirectory, ref STARTUPINFO startupInfo,
118
- out PROCESS_INFORMATION processInformation);
119
- [DllImport("kernel32.dll", SetLastError = true)]
120
- static extern IntPtr GetStdHandle(int which);
121
- [DllImport("kernel32.dll", SetLastError = true)]
122
- static extern bool CloseHandle(IntPtr handle);
123
- [DllImport("kernel32.dll", SetLastError = true)]
124
- static extern uint WaitForMultipleObjects(uint count, IntPtr[] handles, bool waitAll, uint milliseconds);
125
- [DllImport("kernel32.dll", SetLastError = true)]
126
- static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
127
- [DllImport("kernel32.dll", SetLastError = true)]
128
- static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode);
129
- [DllImport("kernel32.dll", SetLastError = true)]
130
- static extern uint ResumeThread(IntPtr thread);
131
- [DllImport("kernel32.dll")]
132
- static extern IntPtr GetCurrentProcess();
133
- [DllImport("kernel32.dll")]
134
- static extern int GetCurrentProcessId();
135
- [DllImport("kernel32.dll", SetLastError = true)]
136
- static extern bool GetProcessTimes(IntPtr process, out long creation, out long exit, out long kernel, out long user);
137
- [DllImport("ntdll.dll")]
138
- static extern int NtQueryInformationProcess(IntPtr process, int infoClass, ref PROCESS_BASIC_INFORMATION info, int length, out int returned);
139
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
140
- static extern bool QueryFullProcessImageName(IntPtr process, int flags, StringBuilder name, ref int size);
141
-
142
- // One argument as CommandLineToArgvW reads it back: quoted only when it must be, a quote escaped by
143
- // backslashes per the Windows rules, so a path with spaces stays ONE argument.
144
- public static string QuoteArg(string arg)
145
- {
146
- if (arg.Length > 0 && arg.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) < 0)
147
- {
148
- return arg;
149
- }
150
- StringBuilder sb = new StringBuilder("\"");
151
- int slashes = 0;
152
- foreach (char ch in arg)
153
- {
154
- if (ch == '\\') { slashes++; continue; }
155
- if (ch == '"') { sb.Append('\\', 2 * slashes + 1); sb.Append('"'); }
156
- else { sb.Append('\\', slashes); sb.Append(ch); }
157
- slashes = 0;
158
- }
159
- sb.Append('\\', 2 * slashes);
160
- sb.Append('"');
161
- return sb.ToString();
162
- }
163
-
164
- // cmd.exe reads its command line by rules of its own: every character it treats as special, the quote and the
165
- // space included, is escaped with a caret so the whole argument is literal text to it.
166
- static string EscapeCmd(string text)
167
- {
168
- return Regex.Replace(text, "([()\\]\\[%!^\"`<>&|;, *?])", "^$1");
169
- }
170
-
171
- static int ParentOf(IntPtr process)
172
- {
173
- PROCESS_BASIC_INFORMATION info = new PROCESS_BASIC_INFORMATION();
174
- int returned;
175
- if (NtQueryInformationProcess(process, 0, ref info, Marshal.SizeOf(info), out returned) != 0)
176
- {
177
- return 0;
178
- }
179
- return (int)info.InheritedFromUniqueProcessId.ToInt64();
180
- }
181
-
182
- static int ParentPid()
183
- {
184
- return ParentOf(GetCurrentProcess());
185
- }
186
-
187
- static long StartOf(IntPtr process, int pid)
188
- {
189
- long start, ignored;
190
- if (!GetProcessTimes(process, out start, out ignored, out ignored, out ignored))
191
- {
192
- throw new InvalidOperationException("the start time of process " + pid + " could not be read (win32 " + Marshal.GetLastWin32Error() + ")");
193
- }
194
- return start;
195
- }
196
-
197
- // A pid names an ancestor only if that process STARTED BEFORE the younger one: a recycled pid belongs to a
198
- // younger process, and then the real ancestor is already dead.
199
- static IntPtr OpenGone(int pid, uint access)
200
- {
201
- IntPtr process = OpenProcess(access, false, pid);
202
- if (process == IntPtr.Zero)
203
- {
204
- int why = Marshal.GetLastWin32Error();
205
- if (why != ERROR_INVALID_PARAMETER)
206
- {
207
- throw new InvalidOperationException("process " + pid + " could not be opened (win32 " + why + ")");
208
- }
209
- }
210
- return process;
211
- }
212
-
213
- static IntPtr OpenOlderThan(int pid, IntPtr younger, int youngerPid)
214
- {
215
- IntPtr process = OpenGone(pid, SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION);
216
- if (process == IntPtr.Zero)
217
- {
218
- return process;
219
- }
220
- if (StartOf(process, pid) > StartOf(younger, youngerPid))
221
- {
222
- CloseHandle(process);
223
- return IntPtr.Zero;
224
- }
225
- return process;
226
- }
227
-
228
- static IntPtr OpenOwner(int ownerPid, bool mustPredateSelf)
229
- {
230
- return mustPredateSelf
231
- ? OpenOlderThan(ownerPid, GetCurrentProcess(), GetCurrentProcessId())
232
- : OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, false, ownerPid);
233
- }
234
-
235
- static string ImageOf(IntPtr process, int pid)
236
- {
237
- StringBuilder name = new StringBuilder(1024);
238
- int size = name.Capacity;
239
- if (!QueryFullProcessImageName(process, 0, name, ref size))
240
- {
241
- throw new InvalidOperationException("the image of process " + pid + " could not be read (win32 " + Marshal.GetLastWin32Error() + ")");
242
- }
243
- return name.ToString(0, size);
244
- }
245
-
246
- static List<IntPtr> ShellFrame()
247
- {
248
- List<IntPtr> frame = new List<IntPtr>();
249
- IntPtr child = GetCurrentProcess();
250
- int childPid = GetCurrentProcessId();
251
- while (true)
252
- {
253
- int parentPid = ParentOf(child);
254
- if (parentPid == 0)
255
- {
256
- throw new InvalidOperationException("the parent of process " + childPid + " could not be read");
257
- }
258
- IntPtr parent = OpenOlderThan(parentPid, child, childPid);
259
- if (parent == IntPtr.Zero)
260
- {
261
- return frame;
262
- }
263
- frame.Add(parent);
264
- string image = Path.GetFileName(ImageOf(parent, parentPid)).ToLowerInvariant();
265
- if (image != "bash.exe" && image != "sh.exe")
266
- {
267
- return frame;
268
- }
269
- child = parent;
270
- childPid = parentPid;
271
- }
272
- }
273
-
274
- static string ReadLock(string path)
275
- {
276
- try
277
- {
278
- using (StreamReader reader = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)))
279
- {
280
- return reader.ReadToEnd().Trim();
281
- }
282
- }
283
- catch (FileNotFoundException)
284
- {
285
- return null;
286
- }
287
- }
288
-
289
- static bool StillHolds(int pid, long start)
290
- {
291
- IntPtr process = OpenGone(pid, PROCESS_QUERY_LIMITED_INFORMATION);
292
- if (process == IntPtr.Zero)
293
- {
294
- return false;
295
- }
296
- uint code;
297
- bool alive = StartOf(process, pid) == start && GetExitCodeProcess(process, out code) && code == STILL_ACTIVE;
298
- CloseHandle(process);
299
- return alive;
300
- }
301
-
302
- static bool ParseLock(string held, out int pid, out long start)
303
- {
304
- string[] parts = held.Split(' ');
305
- start = 0;
306
- pid = 0;
307
- return parts.Length == 2 && int.TryParse(parts[0], out pid) && long.TryParse(parts[1], out start);
308
- }
309
-
310
- static int TakeLock(string path, out string mine)
311
- {
312
- mine = GetCurrentProcessId() + " " + StartOf(GetCurrentProcess(), GetCurrentProcessId());
313
- for (int attempt = 0; attempt < 2; attempt++)
314
- {
315
- if (!File.Exists(path))
316
- {
317
- try
318
- {
319
- using (FileStream file = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read))
320
- {
321
- byte[] bytes = Encoding.ASCII.GetBytes(mine);
322
- file.Write(bytes, 0, bytes.Length);
323
- }
324
- return 0;
325
- }
326
- catch (IOException)
327
- {
328
- if (!File.Exists(path))
329
- {
330
- throw;
331
- }
332
- }
333
- }
334
- string held = ReadLock(path);
335
- int pid;
336
- long start;
337
- if (held != null && !ParseLock(held, out pid, out start))
338
- {
339
- Thread.Sleep(1000);
340
- held = ReadLock(path);
341
- }
342
- if (held == null)
343
- {
344
- continue;
345
- }
346
- bool parsed = ParseLock(held, out pid, out start);
347
- if (parsed && StillHolds(pid, start))
348
- {
349
- Say("REFUSED - an earlier run (pid " + pid + ", started " + DateTime.FromFileTime(start).ToString("yyyy-MM-dd HH:mm:ss") + ") still holds " + path + " - this launch did not run");
350
- return EXIT_ALREADY_RUNNING;
351
- }
352
- try
353
- {
354
- File.Delete(path);
355
- }
356
- catch (IOException e)
357
- {
358
- Say("REFUSED - " + path + " named '" + held + "' and could not be replaced (" + e.Message + ") - this launch did not run");
359
- return EXIT_ALREADY_RUNNING;
360
- }
361
- Say("STALE LOCK - " + path + " named '" + held + "', which " + (parsed ? "no longer runs" : "is not a pid and start time") + " - taken over");
362
- }
363
- Say("REFUSED - " + path + " was taken by another launch while its stale holder was being replaced - this launch did not run");
364
- return EXIT_ALREADY_RUNNING;
365
- }
366
-
367
- static void ReleaseLock(string path, string mine)
368
- {
369
- if (ReadLock(path) == mine)
370
- {
371
- File.Delete(path);
372
- }
373
- }
374
-
375
- static void Say(string text)
376
- {
377
- Console.Error.WriteLine("ownerGuard: " + text);
378
- }
379
-
380
- // The target as the launch has always resolved it (Get-Command -CommandType Application): a name with a folder is
381
- // taken as given, a bare name is searched along PATH, and a name without a PATHEXT extension tries each one.
382
- static string Resolve(string exe)
383
- {
384
- List<string> extensions = new List<string>((Environment.GetEnvironmentVariable("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD")
385
- .Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries));
386
- List<string> folders = new List<string>();
387
- if (exe.IndexOfAny(new char[] { '\\', '/', ':' }) >= 0)
388
- {
389
- folders.Add(null);
390
- }
391
- else
392
- {
393
- foreach (string dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
394
- {
395
- folders.Add(dir.Trim('"'));
396
- }
397
- }
398
- foreach (string folder in folders)
399
- {
400
- string stem = folder == null ? exe : Path.Combine(folder, exe);
401
- if (File.Exists(stem) && extensions.Exists(delegate(string e) { return stem.EndsWith(e, StringComparison.OrdinalIgnoreCase); }))
402
- {
403
- return Path.GetFullPath(stem);
404
- }
405
- foreach (string ext in extensions)
406
- {
407
- if (File.Exists(stem + ext))
408
- {
409
- return Path.GetFullPath(stem + ext);
410
- }
411
- }
412
- }
413
- return null;
414
- }
415
-
416
- // The command line the target is created with, or null (and a refusal said) when it cannot be carried.
417
- static string CommandLine(ref string exe, string[] args, out int refusal)
418
- {
419
- refusal = 0;
420
- string ext = Path.GetExtension(exe).ToLowerInvariant();
421
- StringBuilder line = new StringBuilder();
422
- if (ext == ".cmd" || ext == ".bat")
423
- {
424
- foreach (string arg in args)
425
- {
426
- if (arg.IndexOfAny(new char[] { '\r', '\n' }) >= 0)
427
- {
428
- Say("the target '" + exe + "' is a batch file and one of its arguments holds a line break, which cmd.exe cannot carry - nothing was started");
429
- refusal = EXIT_BATCH_LINE_BREAK;
430
- return null;
431
- }
432
- }
433
- StringBuilder inner = new StringBuilder(EscapeCmd(exe));
434
- foreach (string arg in args)
435
- {
436
- inner.Append(' ').Append(EscapeCmd(EscapeCmd(QuoteArg(arg))));
437
- }
438
- exe = Path.Combine(Environment.SystemDirectory, "cmd.exe");
439
- return QuoteArg(exe) + " /d /s /v:off /c \"" + inner + "\"";
440
- }
441
- line.Append(QuoteArg(exe));
442
- foreach (string arg in args)
443
- {
444
- line.Append(' ').Append(QuoteArg(arg));
445
- }
446
- return line.ToString();
447
- }
448
-
449
- public static int Run(int ownerPid, string exe, string[] args, string cwd)
450
- {
451
- return Run(ownerPid, exe, args, cwd, 0, false, null);
452
- }
453
-
454
- // Runs the target inside a kill-on-close job and answers the target's own exit code. ownerPid <= 0 means
455
- // "the parent process owns this launch", FRAME_OWNERS means "the shell frame owns it" (ShellFrame). cwd null or
456
- // empty starts the target where this process runs. deadlineSeconds > 0 ends the whole job at that age
457
- // (EXIT_DEADLINE). contain leaves BREAKAWAY_OK off, so no process inside can leave the job, Git Bash's
458
- // included. lockFile, when given, admits one launch at a time (TakeLock).
459
- public static int Run(int ownerPid, string exe, string[] args, string cwd, int deadlineSeconds, bool contain, string lockFile)
460
- {
461
- if (string.IsNullOrEmpty(exe) || exe.Trim().Length == 0)
462
- {
463
- Say("the target executable is empty - nothing was started");
464
- return EXIT_NO_TARGET;
465
- }
466
- if (string.IsNullOrEmpty(cwd))
467
- {
468
- cwd = null;
469
- }
470
- string resolved = Resolve(exe);
471
- if (resolved == null)
472
- {
473
- Say("the target '" + exe + "' is not an executable on PATH or on disk - nothing was started");
474
- return EXIT_NOT_FOUND;
475
- }
476
- exe = resolved;
477
- int refusal;
478
- string line = CommandLine(ref exe, args, out refusal);
479
- if (line == null)
480
- {
481
- return refusal;
482
- }
483
- string mine = null;
484
- if (!string.IsNullOrEmpty(lockFile))
485
- {
486
- int refused = TakeLock(lockFile, out mine);
487
- if (refused != 0)
488
- {
489
- return refused;
490
- }
491
- }
492
- try
493
- {
494
- return Launch(ownerPid, exe, line, cwd, deadlineSeconds, contain);
495
- }
496
- finally
497
- {
498
- if (mine != null)
499
- {
500
- ReleaseLock(lockFile, mine);
501
- }
502
- }
503
- }
504
-
505
- static int Launch(int ownerPid, string exe, string line, string cwd, int deadlineSeconds, bool contain)
506
- {
507
- bool frame = ownerPid == FRAME_OWNERS;
508
- bool viaParent = !frame && ownerPid <= 0;
509
- if (viaParent)
510
- {
511
- ownerPid = ParentPid();
512
- }
513
-
514
- IntPtr job = CreateJobObject(IntPtr.Zero, null);
515
- if (job == IntPtr.Zero)
516
- {
517
- Say("the job object could not be created (win32 " + Marshal.GetLastWin32Error() + ") - the target was NOT started unguarded");
518
- return EXIT_NO_JOB;
519
- }
520
- JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
521
- limits.BasicLimitInformation.LimitFlags = contain ? KILL_ON_JOB_CLOSE : KILL_ON_JOB_CLOSE | BREAKAWAY_OK;
522
- int size = Marshal.SizeOf(limits);
523
- IntPtr buffer = Marshal.AllocHGlobal(size);
524
- try
525
- {
526
- Marshal.StructureToPtr(limits, buffer, false);
527
- if (!SetInformationJobObject(job, EXTENDED_LIMIT_INFORMATION, buffer, (uint)size))
528
- {
529
- Say("the job could not be told to kill on close (win32 " + Marshal.GetLastWin32Error() + ") - the target was NOT started unguarded");
530
- CloseHandle(job);
531
- return EXIT_NO_LIMIT;
532
- }
533
- }
534
- finally
535
- {
536
- Marshal.FreeHGlobal(buffer);
537
- }
538
-
539
- STARTUPINFO startup = new STARTUPINFO();
540
- startup.cb = Marshal.SizeOf(startup);
541
- IntPtr stdin = GetStdHandle(-10), stdout = GetStdHandle(-11), stderr = GetStdHandle(-12);
542
- if (stdin != IntPtr.Zero && stdout != IntPtr.Zero && stderr != IntPtr.Zero)
543
- {
544
- startup.dwFlags = STARTF_USESTDHANDLES;
545
- startup.hStdInput = stdin;
546
- startup.hStdOutput = stdout;
547
- startup.hStdError = stderr;
548
- }
549
-
550
- PROCESS_INFORMATION process;
551
- if (!CreateProcess(exe, line, IntPtr.Zero, IntPtr.Zero, true, CREATE_SUSPENDED, IntPtr.Zero, cwd, ref startup, out process))
552
- {
553
- Say("the target could not be created (win32 " + Marshal.GetLastWin32Error() + ") - nothing was started");
554
- CloseHandle(job);
555
- return EXIT_NOT_CREATED;
556
- }
557
- if (!AssignProcessToJobObject(job, process.hProcess))
558
- {
559
- int error = Marshal.GetLastWin32Error();
560
- TerminateJobObject(job, EXIT_NOT_ASSIGNED);
561
- CloseHandle(process.hThread);
562
- CloseHandle(process.hProcess);
563
- CloseHandle(job);
564
- Say("the target could not be put under the job that ends it with its owner (win32 " + error + ") - it was stopped rather than left unguarded");
565
- return EXIT_NOT_ASSIGNED;
566
- }
567
-
568
- // The owner is opened AFTER the target sits in the job and BEFORE it runs, so a dead owner stops it unrun.
569
- List<IntPtr> owners = new List<IntPtr>();
570
- if (frame)
571
- {
572
- owners = ShellFrame();
573
- }
574
- else if (ownerPid > 0)
575
- {
576
- IntPtr owner = OpenOwner(ownerPid, viaParent);
577
- if (owner != IntPtr.Zero)
578
- {
579
- owners.Add(owner);
580
- }
581
- }
582
- if (owners.Count == 0)
583
- {
584
- int why = Marshal.GetLastWin32Error();
585
- TerminateJobObject(job, EXIT_OWNER_GONE);
586
- CloseHandle(process.hThread);
587
- CloseHandle(process.hProcess);
588
- CloseHandle(job);
589
- Say((frame ? "the shell frame above this guard" : "the owner process " + ownerPid) + " cannot be watched (win32 " + why + ") - the target was stopped before it ran");
590
- return EXIT_OWNER_GONE;
591
- }
592
-
593
- if (ResumeThread(process.hThread) == 0xFFFFFFFF)
594
- {
595
- int why = Marshal.GetLastWin32Error();
596
- TerminateJobObject(job, EXIT_NOT_RESUMED);
597
- Say("the target could not be resumed (win32 " + why + ") - it was stopped rather than left suspended");
598
- return EXIT_NOT_RESUMED;
599
- }
600
- CloseHandle(process.hThread);
601
-
602
- List<IntPtr> watched = new List<IntPtr>(owners);
603
- watched.Insert(0, process.hProcess);
604
- uint waited = WaitForMultipleObjects((uint)watched.Count, watched.ToArray(), false, deadlineSeconds > 0 ? (uint)deadlineSeconds * 1000 : INFINITE);
605
- int waitError = Marshal.GetLastWin32Error();
606
- foreach (IntPtr owner in owners)
607
- {
608
- CloseHandle(owner);
609
- }
610
- if (waited == 0xFFFFFFFF)
611
- {
612
- TerminateJobObject(job, EXIT_WAIT_FAILED);
613
- Say("the wait on the target and its owner failed (win32 " + waitError + ") - the target was stopped rather than left unwatched");
614
- return EXIT_WAIT_FAILED;
615
- }
616
- if (waited == WAIT_TIMEOUT)
617
- {
618
- TerminateJobObject(job, EXIT_DEADLINE);
619
- Say("DEADLINE - the target ran past its " + deadlineSeconds + " s; its whole job was ended");
620
- return EXIT_DEADLINE;
621
- }
622
- if (waited != 0)
623
- {
624
- TerminateJobObject(job, EXIT_OWNER_GONE);
625
- Say("OWNER GONE - a process this launch was bound to ended; the target's whole job was ended");
626
- return EXIT_OWNER_GONE;
627
- }
628
-
629
- uint code = 0;
630
- if (!GetExitCodeProcess(process.hProcess, out code))
631
- {
632
- int why = Marshal.GetLastWin32Error();
633
- TerminateJobObject(job, EXIT_NO_EXIT_CODE);
634
- Say("the target's exit code could not be read (win32 " + why + ") - the target was stopped");
635
- return EXIT_NO_EXIT_CODE;
636
- }
637
- TerminateJobObject(job, 1);
638
- CloseHandle(process.hProcess);
639
- CloseHandle(job);
640
- return (int)code;
641
- }
642
-
643
- public static int Main(string[] argv)
644
- {
645
- List<string> rest = new List<string>(argv);
646
- int ownerPid = 0;
647
- if (rest.Count >= 2 && rest[0] == "--owner")
648
- {
649
- if (!int.TryParse(rest[1], out ownerPid) || ownerPid <= 0)
650
- {
651
- Say("--owner needs a process id");
652
- return EXIT_NO_TARGET;
653
- }
654
- rest.RemoveRange(0, 2);
655
- }
656
- if (rest.Count == 0)
657
- {
658
- Say("usage: ownerGuard.exe [--owner <pid>] <exe> [args...]");
659
- return EXIT_NO_TARGET;
660
- }
661
- string exe = rest[0];
662
- rest.RemoveAt(0);
663
- return Run(ownerPid, exe, rest.ToArray(), null);
664
- }
665
- }
@@ -1,31 +0,0 @@
1
- # OWNER-BOUND LAUNCH (#1968): start one target so that it can never outlive its owner, and so that nothing it
2
- # started can outlive IT. This script is only the PowerShell door onto ownerGuard.cs, which holds the whole job.
3
- # #2925: the same source is no longer compiled into an ownerGuard.exe for `claudeCode.claudeProcessWrapper` — that
4
- # build ran csc.exe at every activation, and the .NET compiler is not a dependency a marketplace install may carry.
5
- # The target's own exit code is answered as this script's exit code.
6
- param(
7
- [Parameter(Mandatory = $true)][int]$OwnerPid,
8
- [Parameter(Mandatory = $true)][string]$ExeB64,
9
- [Parameter(Mandatory = $true)][string]$ArgsB64,
10
- [Parameter(Mandatory = $true)][AllowEmptyString()][string]$CwdB64,
11
- [switch]$ShellFrame,
12
- [int]$DeadlineSeconds = 0,
13
- [switch]$Contain,
14
- [string]$LockFile = ""
15
- )
16
-
17
- $ErrorActionPreference = "Stop"
18
-
19
- Add-Type -Path (Join-Path $PSScriptRoot "ownerGuard.cs")
20
-
21
- $exe = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($ExeB64))
22
- # ConvertFrom-Json hands back the WHOLE array as one object on Windows PowerShell, so iterate it with
23
- # foreach: a one-element array arrives as the element itself and a longer one as the array, both right.
24
- $argv = New-Object System.Collections.Generic.List[string]
25
- foreach ($item in ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($ArgsB64)) | ConvertFrom-Json)) {
26
- $argv.Add([string]$item)
27
- }
28
- $cwd = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($CwdB64))
29
-
30
- if ($ShellFrame) { $OwnerPid = [OwnerJob]::FRAME_OWNERS }
31
- exit ([OwnerJob]::Run($OwnerPid, $exe, $argv.ToArray(), $cwd, $DeadlineSeconds, $Contain.IsPresent, $LockFile))