cabloy 5.1.216 → 5.1.218
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/.cabloy-version +1 -1
- package/.github/workflows/create-cabloy.yml +137 -0
- package/.github/workflows/playwright-e2e.yml +163 -13
- package/.github/workflows/vona-test-sqlite3.yml +77 -7
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +4 -2
- package/package.json +1 -1
- package/repo-docs/frontend/scripts.md +2 -0
- package/repo-docs/reference/repo-scripts.md +27 -1
- package/repo-e2e/scripts/e2e.test.ts +1 -1
- package/repo-e2e/scripts/managedTest.test.ts +115 -3
- package/repo-e2e/scripts/managedTest.ts +69 -20
- package/repo-e2e/scripts/managedTestImports.test.ts +55 -5
- package/repo-e2e/scripts/runE2e.test.ts +109 -0
- package/repo-e2e/scripts/runE2e.ts +58 -39
- package/repo-e2e/scripts/runE2eArgs.test.ts +16 -1
- package/repo-e2e/scripts/runE2eArgs.ts +5 -0
- package/repo-e2e/specs/a-commerce.spec.ts +10 -2
- package/repo-e2e/specs/home-user-account.spec.ts +4 -0
- package/repo-e2e/specs/markdown.spec.ts +14 -3
- package/scripts/generateOpenapiIsolated.test.ts +5 -1
- package/scripts/runIsolatedTest.test.ts +403 -36
- package/scripts/runIsolatedTest.ts +139 -12
- package/scripts/testBrowserGuardianWindows.test.ts +327 -0
- package/scripts/testClaimCleanup.test.ts +25 -9
- package/scripts/testClaimCleanup.ts +14 -45
- package/scripts/testClaimProcesses.test.ts +81 -2
- package/scripts/testClaimProcesses.ts +46 -10
- package/scripts/testProcessGuardianWindows.test.ts +602 -0
- package/scripts/testProcessGuardianWindows.ts +155 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +4 -1
- package/vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts +159 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +298 -17
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +205 -97
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts +247 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts +224 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.cs +229 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ts +200 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.cs +336 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts +531 -0
- package/vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts +61 -0
- package/vona/packages-vona/vona-core/src/lib/framework/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts +59 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.ts +8 -5
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts +84 -0
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +2 -2
- package/vona/packages-vona/vona-core/src/lib/utils/util.ts +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +42 -181
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +2 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +16 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +6 -3
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +80 -0
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +73 -2
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +309 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionAsyncLocalStorage_.ts +3 -3
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/src/bean/middlewareSystem.static.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts +101 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +4 -4
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-utils/zova-vite/package.json +2 -2
- package/zova/packages-utils/zova-vite/src/configUtils.ts +1 -1
- package/zova/packages-utils/zova-vite/test/configUtils.test.ts +55 -0
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/pnpm-lock.yaml +324 -63
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +8 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/test/lib/cartModel.test.ts +71 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.ComponentModel;
|
|
4
|
+
using System.Diagnostics;
|
|
5
|
+
using System.Globalization;
|
|
6
|
+
using System.IO;
|
|
7
|
+
using System.IO.Pipes;
|
|
8
|
+
using System.Runtime.InteropServices;
|
|
9
|
+
using System.Text;
|
|
10
|
+
using System.Text.Json;
|
|
11
|
+
using System.Threading;
|
|
12
|
+
using System.Threading.Tasks;
|
|
13
|
+
using Microsoft.Win32.SafeHandles;
|
|
14
|
+
|
|
15
|
+
namespace Cabloy
|
|
16
|
+
{
|
|
17
|
+
// The PowerShell process, not the application subtree, owns the only Job handle.
|
|
18
|
+
// No fallback to AssignProcessToJobObject: job membership is a creation attribute.
|
|
19
|
+
public static class TestProcessWindowsNative
|
|
20
|
+
{
|
|
21
|
+
const int MaxFrame = 1024 * 1024;
|
|
22
|
+
const uint CREATE_SUSPENDED = 0x4, CREATE_UNICODE_ENVIRONMENT = 0x400;
|
|
23
|
+
const uint EXTENDED_STARTUPINFO_PRESENT = 0x80000, CREATE_NO_WINDOW = 0x08000000;
|
|
24
|
+
const uint STARTF_USESTDHANDLES = 0x100, HANDLE_FLAG_INHERIT = 1;
|
|
25
|
+
const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000, SYNCHRONIZE = 0x100000;
|
|
26
|
+
const uint WAIT_OBJECT_0 = 0, WAIT_TIMEOUT = 258;
|
|
27
|
+
const int ERROR_INVALID_PARAMETER = 87;
|
|
28
|
+
const long PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x20002;
|
|
29
|
+
const long PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x2000D;
|
|
30
|
+
static readonly UTF8Encoding Utf8 = new UTF8Encoding(false, true);
|
|
31
|
+
|
|
32
|
+
[StructLayout(LayoutKind.Sequential)] struct FILETIME { public uint Low, High; }
|
|
33
|
+
[StructLayout(LayoutKind.Sequential)] struct IO_COUNTERS { public ulong A, B, C, D, E, F; }
|
|
34
|
+
[StructLayout(LayoutKind.Sequential)] struct BASIC_LIMIT {
|
|
35
|
+
public long PerProcessUserTimeLimit, PerJobUserTimeLimit;
|
|
36
|
+
public uint LimitFlags;
|
|
37
|
+
public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize;
|
|
38
|
+
public uint ActiveProcessLimit;
|
|
39
|
+
public UIntPtr Affinity;
|
|
40
|
+
public uint PriorityClass, SchedulingClass;
|
|
41
|
+
}
|
|
42
|
+
[StructLayout(LayoutKind.Sequential)] struct EXTENDED_LIMIT {
|
|
43
|
+
public BASIC_LIMIT BasicLimitInformation;
|
|
44
|
+
public IO_COUNTERS IoInfo;
|
|
45
|
+
public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
|
|
46
|
+
}
|
|
47
|
+
[StructLayout(LayoutKind.Sequential)] struct BASIC_ACCOUNTING {
|
|
48
|
+
public long TotalUserTime, TotalKernelTime, ThisPeriodTotalUserTime, ThisPeriodTotalKernelTime;
|
|
49
|
+
public uint TotalPageFaultCount, TotalProcesses, ActiveProcesses, TotalTerminatedProcesses;
|
|
50
|
+
}
|
|
51
|
+
[StructLayout(LayoutKind.Sequential)] struct SECURITY_ATTRIBUTES {
|
|
52
|
+
public int Length;
|
|
53
|
+
public IntPtr SecurityDescriptor;
|
|
54
|
+
[MarshalAs(UnmanagedType.Bool)] public bool InheritHandle;
|
|
55
|
+
}
|
|
56
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct STARTUPINFO {
|
|
57
|
+
public int cb;
|
|
58
|
+
public string Reserved, Desktop, Title;
|
|
59
|
+
public uint X, Y, XSize, YSize, XCountChars, YCountChars, FillAttribute, Flags;
|
|
60
|
+
public short ShowWindow, Reserved2;
|
|
61
|
+
public IntPtr Reserved2Pointer, StdInput, StdOutput, StdError;
|
|
62
|
+
}
|
|
63
|
+
[StructLayout(LayoutKind.Sequential)] struct STARTUPINFOEX {
|
|
64
|
+
public STARTUPINFO StartupInfo;
|
|
65
|
+
public IntPtr AttributeList;
|
|
66
|
+
}
|
|
67
|
+
[StructLayout(LayoutKind.Sequential)] struct PROCESS_INFORMATION {
|
|
68
|
+
public IntPtr Process, Thread;
|
|
69
|
+
public uint ProcessId, ThreadId;
|
|
70
|
+
}
|
|
71
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern IntPtr CreateJobObjectW(IntPtr attributes, string name);
|
|
72
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref EXTENDED_LIMIT info, uint length);
|
|
73
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool QueryInformationJobObject(IntPtr job, int infoClass, out BASIC_ACCOUNTING info, uint length, IntPtr returned);
|
|
74
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateJobObject(IntPtr job, uint code);
|
|
75
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool IsProcessInJob(IntPtr process, IntPtr job, [MarshalAs(UnmanagedType.Bool)] out bool result);
|
|
76
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags);
|
|
77
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle);
|
|
78
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool InitializeProcThreadAttributeList(IntPtr list, int count, int flags, ref IntPtr size);
|
|
79
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool UpdateProcThreadAttribute(IntPtr list, uint flags, IntPtr attribute, IntPtr value, IntPtr size, IntPtr previous, IntPtr returned);
|
|
80
|
+
[DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
|
|
81
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern bool CreateProcessW(string application, StringBuilder command, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string cwd, ref STARTUPINFOEX startup, out PROCESS_INFORMATION info);
|
|
82
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread);
|
|
83
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
|
84
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code);
|
|
85
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool GetProcessTimes(IntPtr process, out FILETIME creation, out FILETIME exit, out FILETIME kernel, out FILETIME user);
|
|
86
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
|
|
87
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
|
88
|
+
[DllImport("kernel32.dll")] static extern uint GetCurrentProcessId();
|
|
89
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool CreatePipe(out IntPtr read, out IntPtr write, ref SECURITY_ATTRIBUTES attributes, uint size);
|
|
90
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern IntPtr CreateFileW(string path, uint access, uint share, ref SECURITY_ATTRIBUTES attributes, uint creation, uint flags, IntPtr template);
|
|
91
|
+
|
|
92
|
+
static Exception Native(string operation) { return new Win32Exception(Marshal.GetLastWin32Error(), operation); }
|
|
93
|
+
static void Check(bool ok, string operation) { if (!ok) throw Native(operation); }
|
|
94
|
+
static void Close(ref IntPtr handle) { if (handle != IntPtr.Zero && handle != new IntPtr(-1)) CloseHandle(handle); handle = IntPtr.Zero; }
|
|
95
|
+
static string CreationTime(IntPtr handle) {
|
|
96
|
+
FILETIME creation, exit, kernel, user;
|
|
97
|
+
Check(GetProcessTimes(handle, out creation, out exit, out kernel, out user), "GetProcessTimes");
|
|
98
|
+
return (((ulong)creation.High << 32) | creation.Low).ToString(CultureInfo.InvariantCulture);
|
|
99
|
+
}
|
|
100
|
+
static void Emit(object value) { Console.Out.WriteLine(JsonSerializer.Serialize(value)); Console.Out.Flush(); }
|
|
101
|
+
static string ReadFrame() {
|
|
102
|
+
var text = new StringBuilder();
|
|
103
|
+
for (;;) {
|
|
104
|
+
int ch = Console.In.Read();
|
|
105
|
+
if (ch == -1) { if (text.Length != 0) throw new IOException("Truncated native control frame"); return null; }
|
|
106
|
+
if (ch == '\n') return text.ToString();
|
|
107
|
+
if (text.Length >= MaxFrame) throw new IOException("Native control frame exceeds limit");
|
|
108
|
+
text.Append((char)ch);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
static JsonDocument RequiredFrame() {
|
|
112
|
+
var frame = ReadFrame();
|
|
113
|
+
if (frame == null) throw new IOException("Parent disconnected before launch authorization");
|
|
114
|
+
return JsonDocument.Parse(frame, new JsonDocumentOptions { MaxDepth = 16 });
|
|
115
|
+
}
|
|
116
|
+
static bool Exited(IntPtr process) {
|
|
117
|
+
uint state = WaitForSingleObject(process, 0);
|
|
118
|
+
if (state == WAIT_OBJECT_0) return true;
|
|
119
|
+
if (state == WAIT_TIMEOUT) return false;
|
|
120
|
+
throw Native("WaitForSingleObject");
|
|
121
|
+
}
|
|
122
|
+
static uint Active(IntPtr job) {
|
|
123
|
+
BASIC_ACCOUNTING info;
|
|
124
|
+
Check(QueryInformationJobObject(job, 1, out info, (uint)Marshal.SizeOf<BASIC_ACCOUNTING>(), IntPtr.Zero), "QueryInformationJobObject");
|
|
125
|
+
return info.ActiveProcesses;
|
|
126
|
+
}
|
|
127
|
+
static string Quote(string value) {
|
|
128
|
+
if (value.IndexOf('\0') >= 0) throw new ArgumentException("NUL in process argument");
|
|
129
|
+
var result = new StringBuilder("\"");
|
|
130
|
+
int slashes = 0;
|
|
131
|
+
foreach (char ch in value) {
|
|
132
|
+
if (ch == '\\') { slashes++; continue; }
|
|
133
|
+
result.Append('\\', ch == '"' ? slashes * 2 + 1 : slashes);
|
|
134
|
+
result.Append(ch); slashes = 0;
|
|
135
|
+
}
|
|
136
|
+
result.Append('\\', slashes * 2); result.Append('"'); return result.ToString();
|
|
137
|
+
}
|
|
138
|
+
static string EnvironmentBlock(JsonElement env) {
|
|
139
|
+
var sorted = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
140
|
+
foreach (var entry in env.EnumerateObject()) {
|
|
141
|
+
string value = entry.Value.GetString();
|
|
142
|
+
if (entry.Name.Length == 0 || entry.Name.IndexOfAny(new char[] {'=', '\0'}) >= 0 || value == null || value.IndexOf('\0') >= 0)
|
|
143
|
+
throw new ArgumentException("Invalid Windows process environment");
|
|
144
|
+
sorted.Add(entry.Name, value);
|
|
145
|
+
}
|
|
146
|
+
var text = new StringBuilder();
|
|
147
|
+
foreach (var entry in sorted) text.Append(entry.Key).Append('=').Append(entry.Value).Append('\0');
|
|
148
|
+
// Even an empty environment needs two terminating UTF-16 NULs.
|
|
149
|
+
if (sorted.Count == 0) text.Append('\0');
|
|
150
|
+
return text.Append('\0').ToString();
|
|
151
|
+
}
|
|
152
|
+
static FileStream ReadPipe(out IntPtr write) {
|
|
153
|
+
var attributes = new SECURITY_ATTRIBUTES { Length = Marshal.SizeOf<SECURITY_ATTRIBUTES>(), InheritHandle = true };
|
|
154
|
+
IntPtr read;
|
|
155
|
+
Check(CreatePipe(out read, out write, ref attributes, 0), "CreatePipe");
|
|
156
|
+
try {
|
|
157
|
+
Check(SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0), "SetHandleInformation(pipe)");
|
|
158
|
+
return new FileStream(new SafeFileHandle(read, true), FileAccess.Read, 4096, false);
|
|
159
|
+
} catch { Close(ref read); Close(ref write); throw; }
|
|
160
|
+
}
|
|
161
|
+
static NamedPipeClientStream ConnectOutput(string name) {
|
|
162
|
+
if (!System.Text.RegularExpressions.Regex.IsMatch(name, "^cabloy-test-[a-f0-9]{64}-(out|err)$")) throw new ArgumentException("Invalid output pipe name");
|
|
163
|
+
var pipe = new NamedPipeClientStream(".", name, PipeDirection.Out, PipeOptions.Asynchronous);
|
|
164
|
+
try { pipe.Connect(10000); return pipe; } catch { pipe.Dispose(); throw; }
|
|
165
|
+
}
|
|
166
|
+
static int Inspect() {
|
|
167
|
+
using (var request = RequiredFrame()) {
|
|
168
|
+
var rows = new List<object>();
|
|
169
|
+
foreach (var item in request.RootElement.GetProperty("pids").EnumerateArray()) {
|
|
170
|
+
uint pid = item.GetUInt32();
|
|
171
|
+
if (pid == 0) throw new ArgumentException("Invalid process PID");
|
|
172
|
+
IntPtr handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, false, pid);
|
|
173
|
+
if (handle == IntPtr.Zero) {
|
|
174
|
+
int error = Marshal.GetLastWin32Error();
|
|
175
|
+
if (error == ERROR_INVALID_PARAMETER) continue; // Only confirmed absent PIDs are omitted.
|
|
176
|
+
throw new Win32Exception(error, "OpenProcess(" + pid.ToString(CultureInfo.InvariantCulture) + ")");
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
string startTime = CreationTime(handle);
|
|
180
|
+
if (!Exited(handle)) rows.Add(new { pid = pid, startTime = startTime });
|
|
181
|
+
} finally { Close(ref handle); }
|
|
182
|
+
}
|
|
183
|
+
Emit(new { type = "inspection", processes = rows });
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
static void Containment(IntPtr job, string jobId, string frame) {
|
|
188
|
+
using (var request = JsonDocument.Parse(frame, new JsonDocumentOptions { MaxDepth = 16 })) {
|
|
189
|
+
var query = request.RootElement;
|
|
190
|
+
if (query.GetProperty("type").GetString() != "containment" || query.GetProperty("jobId").GetString() != jobId)
|
|
191
|
+
throw new IOException("Invalid native containment request");
|
|
192
|
+
string requestId = query.GetProperty("requestId").GetString();
|
|
193
|
+
string creation = query.GetProperty("startTime").GetString();
|
|
194
|
+
if (!System.Text.RegularExpressions.Regex.IsMatch(requestId ?? "", "^[a-f0-9]{32}$") ||
|
|
195
|
+
!System.Text.RegularExpressions.Regex.IsMatch(creation ?? "", "^[1-9][0-9]{0,19}$"))
|
|
196
|
+
throw new IOException("Invalid native containment identity");
|
|
197
|
+
uint pid = query.GetProperty("pid").GetUInt32();
|
|
198
|
+
if (pid == 0) throw new IOException("Invalid native containment PID");
|
|
199
|
+
bool contained = false;
|
|
200
|
+
IntPtr process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, false, pid);
|
|
201
|
+
// Missing, denied, terminated, mismatched and query errors all deny, never infer membership.
|
|
202
|
+
if (process != IntPtr.Zero) {
|
|
203
|
+
try {
|
|
204
|
+
bool inJob;
|
|
205
|
+
contained = !Exited(process) && CreationTime(process) == creation &&
|
|
206
|
+
IsProcessInJob(process, job, out inJob) && inJob && !Exited(process);
|
|
207
|
+
} catch { contained = false; }
|
|
208
|
+
finally { Close(ref process); }
|
|
209
|
+
}
|
|
210
|
+
Emit(new { type = "containment", requestId = requestId, jobId = jobId, contained = contained });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
static int Job() {
|
|
214
|
+
IntPtr job = IntPtr.Zero, attributes = IntPtr.Zero, jobValue = IntPtr.Zero, handlesValue = IntPtr.Zero, environment = IntPtr.Zero;
|
|
215
|
+
IntPtr outWrite = IntPtr.Zero, errWrite = IntPtr.Zero, input = IntPtr.Zero;
|
|
216
|
+
PROCESS_INFORMATION target = new PROCESS_INFORMATION();
|
|
217
|
+
bool initialized = false;
|
|
218
|
+
using (var request = RequiredFrame()) {
|
|
219
|
+
var config = request.RootElement;
|
|
220
|
+
string jobId = config.GetProperty("jobId").GetString();
|
|
221
|
+
Guid parsed;
|
|
222
|
+
if (!Guid.TryParse(jobId, out parsed)) throw new ArgumentException("Invalid jobId");
|
|
223
|
+
try {
|
|
224
|
+
job = CreateJobObjectW(IntPtr.Zero, null); // unnamed; no other process can open it by name
|
|
225
|
+
if (job == IntPtr.Zero) throw Native("CreateJobObjectW");
|
|
226
|
+
Check(SetHandleInformation(job, HANDLE_FLAG_INHERIT, 0), "SetHandleInformation(job)");
|
|
227
|
+
var limits = new EXTENDED_LIMIT();
|
|
228
|
+
limits.BasicLimitInformation.LimitFlags = 0x2000; // KILL_ON_JOB_CLOSE, explicitly no breakaway flags
|
|
229
|
+
Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf<EXTENDED_LIMIT>()), "SetInformationJobObject");
|
|
230
|
+
Emit(new { type = "ready", jobId = jobId, pid = GetCurrentProcessId(), startTime = CreationTime(GetCurrentProcess()) });
|
|
231
|
+
// The controller publishes the helper identity durably BEFORE sending authorize.
|
|
232
|
+
var authorization = Task.Run(() => RequiredFrame());
|
|
233
|
+
if (!authorization.Wait(30000)) throw new TimeoutException("Launch authorization timed out");
|
|
234
|
+
using (var authorize = authorization.GetAwaiter().GetResult()) {
|
|
235
|
+
if (authorize.RootElement.GetProperty("type").GetString() != "authorize" || authorize.RootElement.GetProperty("jobId").GetString() != jobId)
|
|
236
|
+
throw new IOException("Launch not authorized");
|
|
237
|
+
}
|
|
238
|
+
using (var stdout = ConnectOutput(config.GetProperty("stdoutPipe").GetString()))
|
|
239
|
+
using (var stderr = ConnectOutput(config.GetProperty("stderrPipe").GetString()))
|
|
240
|
+
using (var outRead = ReadPipe(out outWrite))
|
|
241
|
+
using (var errRead = ReadPipe(out errWrite)) {
|
|
242
|
+
var security = new SECURITY_ATTRIBUTES { Length = Marshal.SizeOf<SECURITY_ATTRIBUTES>(), InheritHandle = true };
|
|
243
|
+
input = CreateFileW("NUL", 0x80000000, 3, ref security, 3, 0, IntPtr.Zero);
|
|
244
|
+
if (input == new IntPtr(-1)) throw Native("CreateFileW(NUL)");
|
|
245
|
+
IntPtr size = IntPtr.Zero;
|
|
246
|
+
InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref size);
|
|
247
|
+
if (size == IntPtr.Zero) throw Native("InitializeProcThreadAttributeList(size)");
|
|
248
|
+
attributes = Marshal.AllocHGlobal(size);
|
|
249
|
+
Check(InitializeProcThreadAttributeList(attributes, 2, 0, ref size), "InitializeProcThreadAttributeList"); initialized = true;
|
|
250
|
+
jobValue = Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(jobValue, job);
|
|
251
|
+
// Test-only negative capability probe. It can only prevent launch, never weaken containment.
|
|
252
|
+
if (config.TryGetProperty("rejectJobAssignment", out var reject) && reject.GetBoolean()) Marshal.WriteIntPtr(jobValue, IntPtr.Zero);
|
|
253
|
+
Check(UpdateProcThreadAttribute(attributes, 0, new IntPtr(PROC_THREAD_ATTRIBUTE_JOB_LIST), jobValue, new IntPtr(IntPtr.Size), IntPtr.Zero, IntPtr.Zero), "UpdateProcThreadAttribute(JOB_LIST)");
|
|
254
|
+
handlesValue = Marshal.AllocHGlobal(IntPtr.Size * 3);
|
|
255
|
+
Marshal.WriteIntPtr(handlesValue, 0, input); Marshal.WriteIntPtr(handlesValue, IntPtr.Size, outWrite); Marshal.WriteIntPtr(handlesValue, IntPtr.Size * 2, errWrite);
|
|
256
|
+
Check(UpdateProcThreadAttribute(attributes, 0, new IntPtr(PROC_THREAD_ATTRIBUTE_HANDLE_LIST), handlesValue, new IntPtr(IntPtr.Size * 3), IntPtr.Zero, IntPtr.Zero), "UpdateProcThreadAttribute(HANDLE_LIST)");
|
|
257
|
+
var startup = new STARTUPINFOEX();
|
|
258
|
+
startup.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>(); startup.StartupInfo.Flags = STARTF_USESTDHANDLES;
|
|
259
|
+
startup.StartupInfo.StdInput = input; startup.StartupInfo.StdOutput = outWrite; startup.StartupInfo.StdError = errWrite; startup.AttributeList = attributes;
|
|
260
|
+
string command = config.GetProperty("command").GetString();
|
|
261
|
+
if (!Path.IsPathFullyQualified(command) || !File.Exists(command) || !command.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Windows owned command must be an absolute .exe path; launch npm via node's npm-cli.js");
|
|
262
|
+
var commandLine = new StringBuilder(Quote(command));
|
|
263
|
+
foreach (var argument in config.GetProperty("args").EnumerateArray()) commandLine.Append(' ').Append(Quote(argument.GetString()));
|
|
264
|
+
if (commandLine.Length >= 32767) throw new ArgumentException("Windows command line too long");
|
|
265
|
+
environment = Marshal.StringToHGlobalUni(EnvironmentBlock(config.GetProperty("env")));
|
|
266
|
+
Check(CreateProcessW(command, commandLine, IntPtr.Zero, IntPtr.Zero, true,
|
|
267
|
+
CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
|
|
268
|
+
environment, config.GetProperty("cwd").GetString(), ref startup, out target), "CreateProcessW(JOB_LIST)");
|
|
269
|
+
Close(ref outWrite); Close(ref errWrite); Close(ref input);
|
|
270
|
+
// Creation-time membership is already established. A crash even here closes the Job and kills the suspended process.
|
|
271
|
+
if (Active(job) != 1) throw new IOException("Created process was not accounted in the owned Job");
|
|
272
|
+
var outputTask = outRead.CopyToAsync(stdout);
|
|
273
|
+
var errorTask = errRead.CopyToAsync(stderr);
|
|
274
|
+
if (ResumeThread(target.Thread) == UInt32.MaxValue) throw Native("ResumeThread");
|
|
275
|
+
Close(ref target.Thread);
|
|
276
|
+
Emit(new { type = "started" });
|
|
277
|
+
var control = Task.Run(() => ReadFrame());
|
|
278
|
+
bool failed = false, terminated = false;
|
|
279
|
+
long rootExitAt = -1, stopAt = -1;
|
|
280
|
+
var clock = Stopwatch.StartNew();
|
|
281
|
+
for (;;) {
|
|
282
|
+
bool rootExited = Exited(target.Process);
|
|
283
|
+
uint active = Active(job);
|
|
284
|
+
if (rootExited && rootExitAt < 0) rootExitAt = clock.ElapsedMilliseconds;
|
|
285
|
+
if (control.IsCompleted && !terminated) {
|
|
286
|
+
bool disconnect = false;
|
|
287
|
+
try {
|
|
288
|
+
string frame = control.GetAwaiter().GetResult();
|
|
289
|
+
if (frame == null) disconnect = true;
|
|
290
|
+
else {
|
|
291
|
+
Containment(job, jobId, frame);
|
|
292
|
+
control = Task.Run(() => ReadFrame());
|
|
293
|
+
}
|
|
294
|
+
} catch { disconnect = true; }
|
|
295
|
+
if (disconnect) {
|
|
296
|
+
// EOF, explicit stop, malformed input, and reader faults still terminate.
|
|
297
|
+
failed = true; terminated = true; stopAt = clock.ElapsedMilliseconds;
|
|
298
|
+
Check(TerminateJobObject(job, 1), "TerminateJobObject(parent stop/disconnect)");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (rootExited && active == 0) break;
|
|
302
|
+
if (!terminated && rootExitAt >= 0 && clock.ElapsedMilliseconds - rootExitAt >= 1000) {
|
|
303
|
+
failed = true; terminated = true; stopAt = clock.ElapsedMilliseconds;
|
|
304
|
+
Check(TerminateJobObject(job, 1), "TerminateJobObject(remaining descendants)");
|
|
305
|
+
}
|
|
306
|
+
if (terminated && clock.ElapsedMilliseconds - stopAt > 10000) throw new TimeoutException("Owned Windows Job did not drain");
|
|
307
|
+
Thread.Sleep(25);
|
|
308
|
+
}
|
|
309
|
+
uint exitCode; Check(GetExitCodeProcess(target.Process, out exitCode), "GetExitCodeProcess");
|
|
310
|
+
if (!Task.WaitAll(new Task[] { outputTask, errorTask }, 10000)) throw new TimeoutException("Owned process output did not drain");
|
|
311
|
+
stdout.Flush(); stderr.Flush();
|
|
312
|
+
Emit(new { type = "drained", code = exitCode, failed = failed });
|
|
313
|
+
return 0; // Controller must observe this successful helper exit before durable drainage.
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
// Sole Job handle always closes, including assignment/resume/query/output failures.
|
|
317
|
+
Close(ref job); Close(ref target.Thread); Close(ref target.Process);
|
|
318
|
+
Close(ref input); Close(ref outWrite); Close(ref errWrite);
|
|
319
|
+
if (initialized) DeleteProcThreadAttributeList(attributes);
|
|
320
|
+
if (attributes != IntPtr.Zero) Marshal.FreeHGlobal(attributes);
|
|
321
|
+
if (jobValue != IntPtr.Zero) Marshal.FreeHGlobal(jobValue);
|
|
322
|
+
if (handlesValue != IntPtr.Zero) Marshal.FreeHGlobal(handlesValue);
|
|
323
|
+
if (environment != IntPtr.Zero) Marshal.FreeHGlobal(environment);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
public static int Run(string mode) {
|
|
328
|
+
try {
|
|
329
|
+
Console.InputEncoding = Utf8; Console.OutputEncoding = Utf8;
|
|
330
|
+
if (mode == "inspect") return Inspect();
|
|
331
|
+
if (mode == "job") return Job();
|
|
332
|
+
throw new ArgumentException("Invalid native process mode");
|
|
333
|
+
} catch (Exception error) { Console.Error.WriteLine(error.ToString()); return 1; }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# PowerShell 7 is an explicit runtime dependency; this file is never shell-interpolated.
|
|
2
|
+
# stdout belongs exclusively to the bounded native JSON control protocol.
|
|
3
|
+
param(
|
|
4
|
+
[Parameter(Mandatory = $true)]
|
|
5
|
+
[ValidateSet('job', 'inspect')]
|
|
6
|
+
[string] $Mode
|
|
7
|
+
)
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
10
|
+
try {
|
|
11
|
+
if ($PSVersionTable.PSVersion.Major -lt 7 -or -not $IsWindows) {
|
|
12
|
+
throw 'The isolated Windows process backend requires PowerShell 7 on native Windows.'
|
|
13
|
+
}
|
|
14
|
+
Add-Type -Path (Join-Path $PSScriptRoot 'testProcessWindowsNative.cs') -ErrorAction Stop
|
|
15
|
+
$code = [Cabloy.TestProcessWindowsNative]::Run($Mode)
|
|
16
|
+
exit $code
|
|
17
|
+
} catch {
|
|
18
|
+
[Console]::Error.WriteLine($_.Exception.ToString())
|
|
19
|
+
exit 1
|
|
20
|
+
}
|