@openclaw/crabline 0.1.11 → 0.1.13
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/README.md +137 -23
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +281 -52
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/index.d.ts +12 -10
- package/dist/src/index.js +6 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +7 -6
- package/dist/src/openclaw/artifact-generation.js +385 -41
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/discord.d.ts +1 -0
- package/dist/src/openclaw/bridges/discord.js +149 -0
- package/dist/src/openclaw/bridges/discord.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +22 -5
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +14 -9
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +16 -7
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +117 -24
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +46 -20
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +33 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +9 -4
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.js +3 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
- package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
- package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +10 -17
- package/dist/src/openclaw/shared.js +53 -19
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw.d.ts +3 -5
- package/dist/src/openclaw.js +245 -60
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +521 -41
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/discord.d.ts +40 -0
- package/dist/src/servers/discord.js +842 -0
- package/dist/src/servers/discord.js.map +1 -0
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.d.ts +10 -4
- package/dist/src/servers/index.js +4 -0
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1072 -69
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +270 -54
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1218 -107
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -1
- package/dist/src/servers/whatsapp.js +149 -25
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +251 -64
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/dist/src/openclaw/smoke-lock.js +0 -1004
- package/dist/src/openclaw/smoke-lock.js.map +0 -1
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -1,10 +1,512 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { execFile, spawn, } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { CrablineError, ensureErrorMessage } from "../../core/errors.js";
|
|
5
7
|
const MAX_SCRIPT_OUTPUT_BYTES = 1024 * 1024;
|
|
6
8
|
const SCRIPT_WAIT_EXIT_GRACE_MS = 250;
|
|
7
|
-
const
|
|
9
|
+
const CHILD_CLOSE_TIMEOUT_MS = 1_000;
|
|
10
|
+
const WINDOWS_TERMINATION_COMMAND_TIMEOUT_MS = 2_500;
|
|
11
|
+
const WINDOWS_JOB_COMMAND_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_COMMAND";
|
|
12
|
+
const WINDOWS_JOB_SHELL_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_SHELL";
|
|
13
|
+
const WINDOWS_JOB_HELPER_SOURCE_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_HELPER_SOURCE";
|
|
14
|
+
const WINDOWS_JOB_HELPER_OUTPUT_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_HELPER_OUTPUT";
|
|
15
|
+
const WINDOWS_JOB_HELPER_SOURCE = [
|
|
16
|
+
"using System;",
|
|
17
|
+
"using System.ComponentModel;",
|
|
18
|
+
"using System.Runtime.InteropServices;",
|
|
19
|
+
"using System.Text;",
|
|
20
|
+
"public static class CrablineScriptJob{",
|
|
21
|
+
"private const uint CreateSuspended=0x00000004;",
|
|
22
|
+
"private const uint ExtendedStartupInfoPresent=0x00080000;",
|
|
23
|
+
"private const uint Infinite=0xffffffff;",
|
|
24
|
+
"private const uint JobObjectLimitKillOnJobClose=0x00002000;",
|
|
25
|
+
"private const int ProcThreadAttributeJobList=0x0002000d;",
|
|
26
|
+
"private const uint StartfUseStdHandles=0x00000100;",
|
|
27
|
+
"private const uint DuplicateSameAccess=0x00000002;",
|
|
28
|
+
"private const uint ResumeThreadFailed=0xffffffff;",
|
|
29
|
+
"private const int JobObjectExtendedLimitInformationClass=9;",
|
|
30
|
+
"private const int StdInputHandle=-10;",
|
|
31
|
+
"private const int StdOutputHandle=-11;",
|
|
32
|
+
"private const int StdErrorHandle=-12;",
|
|
33
|
+
"[StructLayout(LayoutKind.Sequential)]",
|
|
34
|
+
"private struct ProcessInformation{",
|
|
35
|
+
"public IntPtr Process;",
|
|
36
|
+
"public IntPtr Thread;",
|
|
37
|
+
"public uint ProcessId;",
|
|
38
|
+
"public uint ThreadId;",
|
|
39
|
+
"}",
|
|
40
|
+
"[StructLayout(LayoutKind.Sequential)]",
|
|
41
|
+
"private struct StartupInfo{",
|
|
42
|
+
"public int Size;",
|
|
43
|
+
"public IntPtr Reserved;",
|
|
44
|
+
"public IntPtr Desktop;",
|
|
45
|
+
"public IntPtr Title;",
|
|
46
|
+
"public int X;",
|
|
47
|
+
"public int Y;",
|
|
48
|
+
"public int XSize;",
|
|
49
|
+
"public int YSize;",
|
|
50
|
+
"public int XCountChars;",
|
|
51
|
+
"public int YCountChars;",
|
|
52
|
+
"public int FillAttribute;",
|
|
53
|
+
"public uint Flags;",
|
|
54
|
+
"public short ShowWindow;",
|
|
55
|
+
"public short Reserved2Size;",
|
|
56
|
+
"public IntPtr Reserved2;",
|
|
57
|
+
"public IntPtr StandardInput;",
|
|
58
|
+
"public IntPtr StandardOutput;",
|
|
59
|
+
"public IntPtr StandardError;",
|
|
60
|
+
"}",
|
|
61
|
+
"[StructLayout(LayoutKind.Sequential)]",
|
|
62
|
+
"private struct StartupInfoEx{",
|
|
63
|
+
"public StartupInfo StartupInfo;",
|
|
64
|
+
"public IntPtr AttributeList;",
|
|
65
|
+
"}",
|
|
66
|
+
"[StructLayout(LayoutKind.Sequential)]",
|
|
67
|
+
"private struct JobObjectBasicLimitInformation{",
|
|
68
|
+
"public long PerProcessUserTimeLimit;",
|
|
69
|
+
"public long PerJobUserTimeLimit;",
|
|
70
|
+
"public uint LimitFlags;",
|
|
71
|
+
"public UIntPtr MinimumWorkingSetSize;",
|
|
72
|
+
"public UIntPtr MaximumWorkingSetSize;",
|
|
73
|
+
"public uint ActiveProcessLimit;",
|
|
74
|
+
"public IntPtr Affinity;",
|
|
75
|
+
"public uint PriorityClass;",
|
|
76
|
+
"public uint SchedulingClass;",
|
|
77
|
+
"}",
|
|
78
|
+
"[StructLayout(LayoutKind.Sequential)]",
|
|
79
|
+
"private struct IoCounters{",
|
|
80
|
+
"public ulong ReadOperationCount;",
|
|
81
|
+
"public ulong WriteOperationCount;",
|
|
82
|
+
"public ulong OtherOperationCount;",
|
|
83
|
+
"public ulong ReadTransferCount;",
|
|
84
|
+
"public ulong WriteTransferCount;",
|
|
85
|
+
"public ulong OtherTransferCount;",
|
|
86
|
+
"}",
|
|
87
|
+
"[StructLayout(LayoutKind.Sequential)]",
|
|
88
|
+
"private struct JobObjectExtendedLimitInformation{",
|
|
89
|
+
"public JobObjectBasicLimitInformation BasicLimitInformation;",
|
|
90
|
+
"public IoCounters IoInfo;",
|
|
91
|
+
"public UIntPtr ProcessMemoryLimit;",
|
|
92
|
+
"public UIntPtr JobMemoryLimit;",
|
|
93
|
+
"public UIntPtr PeakProcessMemoryUsed;",
|
|
94
|
+
"public UIntPtr PeakJobMemoryUsed;",
|
|
95
|
+
"}",
|
|
96
|
+
'[DllImport("kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)]',
|
|
97
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
98
|
+
"private static extern bool CreateProcess(string applicationName,StringBuilder commandLine,IntPtr processAttributes,IntPtr threadAttributes,bool inheritHandles,uint creationFlags,IntPtr environment,string currentDirectory,ref StartupInfoEx startupInfo,out ProcessInformation processInformation);",
|
|
99
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
100
|
+
"private static extern IntPtr CreateJobObject(IntPtr jobAttributes,string name);",
|
|
101
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
102
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
103
|
+
"private static extern bool SetInformationJobObject(IntPtr job,int informationClass,ref JobObjectExtendedLimitInformation information,uint informationLength);",
|
|
104
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
105
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
106
|
+
"private static extern bool InitializeProcThreadAttributeList(IntPtr attributeList,int attributeCount,int flags,ref IntPtr size);",
|
|
107
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
108
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
109
|
+
"private static extern bool UpdateProcThreadAttribute(IntPtr attributeList,uint flags,IntPtr attribute,IntPtr value,IntPtr size,IntPtr previousValue,IntPtr returnSize);",
|
|
110
|
+
'[DllImport("kernel32.dll")]',
|
|
111
|
+
"private static extern void DeleteProcThreadAttributeList(IntPtr attributeList);",
|
|
112
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
113
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
114
|
+
"private static extern bool DuplicateHandle(IntPtr sourceProcess,IntPtr sourceHandle,IntPtr targetProcess,out IntPtr targetHandle,uint desiredAccess,bool inheritHandle,uint options);",
|
|
115
|
+
'[DllImport("kernel32.dll")]',
|
|
116
|
+
"private static extern IntPtr GetCurrentProcess();",
|
|
117
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
118
|
+
"private static extern IntPtr GetStdHandle(int standardHandle);",
|
|
119
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
120
|
+
"private static extern uint ResumeThread(IntPtr thread);",
|
|
121
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
122
|
+
"private static extern uint WaitForSingleObject(IntPtr handle,uint milliseconds);",
|
|
123
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
124
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
125
|
+
"private static extern bool GetExitCodeProcess(IntPtr process,out uint exitCode);",
|
|
126
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
127
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
128
|
+
"private static extern bool TerminateProcess(IntPtr process,uint exitCode);",
|
|
129
|
+
'[DllImport("kernel32.dll")]',
|
|
130
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
131
|
+
"private static extern bool CloseHandle(IntPtr handle);",
|
|
132
|
+
"private static void ThrowLastError(string operation){throw new Win32Exception(Marshal.GetLastWin32Error(),operation);}",
|
|
133
|
+
"private static IntPtr DuplicateStandardHandle(int standardHandle){",
|
|
134
|
+
"IntPtr source=GetStdHandle(standardHandle);",
|
|
135
|
+
"IntPtr duplicate=IntPtr.Zero;",
|
|
136
|
+
"IntPtr process=GetCurrentProcess();",
|
|
137
|
+
'if(source==IntPtr.Zero||source==new IntPtr(-1)||!DuplicateHandle(process,source,process,out duplicate,0,true,DuplicateSameAccess)){ThrowLastError("Could not duplicate a script standard handle.");}',
|
|
138
|
+
"return duplicate;",
|
|
139
|
+
"}",
|
|
140
|
+
"private static int Run(string applicationName,string commandLine){",
|
|
141
|
+
"IntPtr job=IntPtr.Zero;",
|
|
142
|
+
"IntPtr attributeList=IntPtr.Zero;",
|
|
143
|
+
"IntPtr jobList=IntPtr.Zero;",
|
|
144
|
+
"IntPtr standardInput=IntPtr.Zero;",
|
|
145
|
+
"IntPtr standardOutput=IntPtr.Zero;",
|
|
146
|
+
"IntPtr standardError=IntPtr.Zero;",
|
|
147
|
+
"ProcessInformation processInformation=new ProcessInformation();",
|
|
148
|
+
"bool processCreated=false;",
|
|
149
|
+
"try{",
|
|
150
|
+
"job=CreateJobObject(IntPtr.Zero,null);",
|
|
151
|
+
'if(job==IntPtr.Zero){ThrowLastError("Could not create the script job object.");}',
|
|
152
|
+
"JobObjectExtendedLimitInformation limits=new JobObjectExtendedLimitInformation();",
|
|
153
|
+
"limits.BasicLimitInformation.LimitFlags=JobObjectLimitKillOnJobClose;",
|
|
154
|
+
'if(!SetInformationJobObject(job,JobObjectExtendedLimitInformationClass,ref limits,(uint)Marshal.SizeOf(typeof(JobObjectExtendedLimitInformation)))){ThrowLastError("Could not configure the script job object.");}',
|
|
155
|
+
"IntPtr attributeListSize=IntPtr.Zero;",
|
|
156
|
+
"InitializeProcThreadAttributeList(IntPtr.Zero,1,0,ref attributeListSize);",
|
|
157
|
+
"attributeList=Marshal.AllocHGlobal(attributeListSize);",
|
|
158
|
+
'if(!InitializeProcThreadAttributeList(attributeList,1,0,ref attributeListSize)){ThrowLastError("Could not initialize the script job attribute list.");}',
|
|
159
|
+
"jobList=Marshal.AllocHGlobal(IntPtr.Size);",
|
|
160
|
+
"Marshal.WriteIntPtr(jobList,job);",
|
|
161
|
+
'if(!UpdateProcThreadAttribute(attributeList,0,new IntPtr(ProcThreadAttributeJobList),jobList,new IntPtr(IntPtr.Size),IntPtr.Zero,IntPtr.Zero)){ThrowLastError("Could not bind the script process to its job object.");}',
|
|
162
|
+
"standardInput=DuplicateStandardHandle(StdInputHandle);",
|
|
163
|
+
"standardOutput=DuplicateStandardHandle(StdOutputHandle);",
|
|
164
|
+
"standardError=DuplicateStandardHandle(StdErrorHandle);",
|
|
165
|
+
"StartupInfoEx startupInfo=new StartupInfoEx();",
|
|
166
|
+
"startupInfo.StartupInfo.Size=Marshal.SizeOf(typeof(StartupInfoEx));",
|
|
167
|
+
"startupInfo.StartupInfo.Flags=StartfUseStdHandles;",
|
|
168
|
+
"startupInfo.StartupInfo.StandardInput=standardInput;",
|
|
169
|
+
"startupInfo.StartupInfo.StandardOutput=standardOutput;",
|
|
170
|
+
"startupInfo.StartupInfo.StandardError=standardError;",
|
|
171
|
+
"startupInfo.AttributeList=attributeList;",
|
|
172
|
+
'if(!CreateProcess(applicationName,new StringBuilder(commandLine),IntPtr.Zero,IntPtr.Zero,true,CreateSuspended|ExtendedStartupInfoPresent,IntPtr.Zero,Environment.CurrentDirectory,ref startupInfo,out processInformation)){ThrowLastError("Could not start the script command.");}',
|
|
173
|
+
"processCreated=true;",
|
|
174
|
+
'if(ResumeThread(processInformation.Thread)==ResumeThreadFailed){ThrowLastError("Could not resume the script command.");}',
|
|
175
|
+
'if(WaitForSingleObject(processInformation.Process,Infinite)==Infinite){ThrowLastError("Could not wait for the script command.");}',
|
|
176
|
+
"uint exitCode;",
|
|
177
|
+
'if(!GetExitCodeProcess(processInformation.Process,out exitCode)){ThrowLastError("Could not read the script exit code.");}',
|
|
178
|
+
"return unchecked((int)exitCode);",
|
|
179
|
+
"}finally{",
|
|
180
|
+
"if(processCreated&&processInformation.Process!=IntPtr.Zero){TerminateProcess(processInformation.Process,1);}",
|
|
181
|
+
"if(processInformation.Thread!=IntPtr.Zero){CloseHandle(processInformation.Thread);}",
|
|
182
|
+
"if(processInformation.Process!=IntPtr.Zero){CloseHandle(processInformation.Process);}",
|
|
183
|
+
"if(standardInput!=IntPtr.Zero){CloseHandle(standardInput);}",
|
|
184
|
+
"if(standardOutput!=IntPtr.Zero){CloseHandle(standardOutput);}",
|
|
185
|
+
"if(standardError!=IntPtr.Zero){CloseHandle(standardError);}",
|
|
186
|
+
"if(attributeList!=IntPtr.Zero){DeleteProcThreadAttributeList(attributeList);Marshal.FreeHGlobal(attributeList);}",
|
|
187
|
+
"if(jobList!=IntPtr.Zero){Marshal.FreeHGlobal(jobList);}",
|
|
188
|
+
"if(job!=IntPtr.Zero){CloseHandle(job);}",
|
|
189
|
+
"}",
|
|
190
|
+
"}",
|
|
191
|
+
"public static int Main(){",
|
|
192
|
+
"try{",
|
|
193
|
+
`string shellValue=Environment.GetEnvironmentVariable("${WINDOWS_JOB_SHELL_ENV}");`,
|
|
194
|
+
`string commandValue=Environment.GetEnvironmentVariable("${WINDOWS_JOB_COMMAND_ENV}");`,
|
|
195
|
+
`Environment.SetEnvironmentVariable("${WINDOWS_JOB_SHELL_ENV}",null);`,
|
|
196
|
+
`Environment.SetEnvironmentVariable("${WINDOWS_JOB_COMMAND_ENV}",null);`,
|
|
197
|
+
'if(String.IsNullOrEmpty(shellValue)||String.IsNullOrEmpty(commandValue)){throw new InvalidOperationException("Missing script job configuration.");}',
|
|
198
|
+
"string shell=Encoding.UTF8.GetString(Convert.FromBase64String(shellValue));",
|
|
199
|
+
"string commandLine=Encoding.UTF8.GetString(Convert.FromBase64String(commandValue));",
|
|
200
|
+
"return Run(shell,commandLine);",
|
|
201
|
+
"}catch(Exception error){Console.Error.WriteLine(error.Message);return 1;}",
|
|
202
|
+
"}",
|
|
203
|
+
"}",
|
|
204
|
+
].join("");
|
|
205
|
+
const WINDOWS_PROCESS_TERMINATOR_SOURCE = [
|
|
206
|
+
"using System;",
|
|
207
|
+
"using System.Runtime.InteropServices;",
|
|
208
|
+
"public static class CrablineProcessTerminator{",
|
|
209
|
+
"private const uint ProcessTerminate=0x0001;",
|
|
210
|
+
"private const uint ProcessQueryLimitedInformation=0x1000;",
|
|
211
|
+
"private const uint Synchronize=0x00100000;",
|
|
212
|
+
"private const uint WaitObject0=0;",
|
|
213
|
+
"private const uint WaitTimeout=258;",
|
|
214
|
+
"private const int ErrorInvalidParameter=87;",
|
|
215
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
216
|
+
"private static extern IntPtr OpenProcess(uint access,bool inheritHandle,int processId);",
|
|
217
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
218
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
219
|
+
"private static extern bool GetProcessTimes(IntPtr process,out long creationTime,out long exitTime,out long kernelTime,out long userTime);",
|
|
220
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
221
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
222
|
+
"private static extern bool TerminateProcess(IntPtr process,uint exitCode);",
|
|
223
|
+
'[DllImport("kernel32.dll",SetLastError=true)]',
|
|
224
|
+
"private static extern uint WaitForSingleObject(IntPtr handle,uint milliseconds);",
|
|
225
|
+
'[DllImport("kernel32.dll")]',
|
|
226
|
+
"[return:MarshalAs(UnmanagedType.Bool)]",
|
|
227
|
+
"private static extern bool CloseHandle(IntPtr handle);",
|
|
228
|
+
"public static bool TerminateVerified(int processId,long expectedCreationTime){",
|
|
229
|
+
"IntPtr handle=OpenProcess(ProcessTerminate|ProcessQueryLimitedInformation|Synchronize,false,processId);",
|
|
230
|
+
"if(handle==IntPtr.Zero){return Marshal.GetLastWin32Error()==ErrorInvalidParameter;}",
|
|
231
|
+
"try{",
|
|
232
|
+
"long creationTime,exitTime,kernelTime,userTime;",
|
|
233
|
+
"if(!GetProcessTimes(handle,out creationTime,out exitTime,out kernelTime,out userTime)){return false;}",
|
|
234
|
+
"if(creationTime/10!=expectedCreationTime/10){return true;}",
|
|
235
|
+
"uint status=WaitForSingleObject(handle,0);",
|
|
236
|
+
"if(status==WaitObject0){return true;}",
|
|
237
|
+
"if(status!=WaitTimeout){return false;}",
|
|
238
|
+
"if(!TerminateProcess(handle,1)){return WaitForSingleObject(handle,0)==WaitObject0;}",
|
|
239
|
+
"return WaitForSingleObject(handle,1000)==WaitObject0;",
|
|
240
|
+
"}finally{CloseHandle(handle);}",
|
|
241
|
+
"}",
|
|
242
|
+
"}",
|
|
243
|
+
].join("");
|
|
244
|
+
let windowsJobHelperPath;
|
|
245
|
+
let windowsJobHelperBootstrap;
|
|
246
|
+
function removeWindowsJobHelper(directory) {
|
|
247
|
+
try {
|
|
248
|
+
rmSync(directory, { force: true, recursive: true });
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// The helper may still be exiting during process shutdown.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function runWindowsJobHelperCommand(executable, args, options, signal) {
|
|
255
|
+
if (signal.aborted) {
|
|
256
|
+
return Promise.reject(signal.reason ?? new Error("Windows script helper bootstrap aborted."));
|
|
257
|
+
}
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const startedAtMs = Date.now();
|
|
260
|
+
let child;
|
|
261
|
+
let settled = false;
|
|
262
|
+
let aborting = false;
|
|
263
|
+
const finish = (error) => {
|
|
264
|
+
if (settled || aborting) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
settled = true;
|
|
268
|
+
signal.removeEventListener("abort", abort);
|
|
269
|
+
if (error) {
|
|
270
|
+
reject(error);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
resolve();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const abort = () => {
|
|
277
|
+
if (settled || aborting) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
aborting = true;
|
|
281
|
+
signal.removeEventListener("abort", abort);
|
|
282
|
+
void (async () => {
|
|
283
|
+
try {
|
|
284
|
+
await terminateChild(child, startedAtMs, observedAtMs);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// The command still needs to be drained before returning the original abort.
|
|
288
|
+
}
|
|
289
|
+
await waitForChildClose(childClosed);
|
|
290
|
+
settled = true;
|
|
291
|
+
reject(signal.reason ?? new Error("Windows script helper bootstrap aborted."));
|
|
292
|
+
})();
|
|
293
|
+
};
|
|
294
|
+
try {
|
|
295
|
+
child = execFile(executable, args, options, (error) => finish(error ?? undefined));
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
reject(error);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const observedAtMs = Date.now();
|
|
302
|
+
const childClosed = new Promise((resolveClose) => {
|
|
303
|
+
child.once("close", (code, closeSignal) => resolveClose({ code, signal: closeSignal }));
|
|
304
|
+
});
|
|
305
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
306
|
+
if (signal.aborted) {
|
|
307
|
+
abort();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async function createWindowsJobHelper(signal) {
|
|
312
|
+
if (windowsJobHelperPath !== undefined) {
|
|
313
|
+
return windowsJobHelperPath;
|
|
314
|
+
}
|
|
315
|
+
let directory;
|
|
316
|
+
try {
|
|
317
|
+
directory = mkdtempSync(path.join(tmpdir(), "crabline-script-job-"));
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
throw new Error("Could not create the Windows script Job Object helper directory.", {
|
|
321
|
+
cause: error,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const helperPath = path.join(directory, "crabline-script-job.exe");
|
|
325
|
+
try {
|
|
326
|
+
await runWindowsJobHelperCommand("powershell.exe", [
|
|
327
|
+
"-NoLogo",
|
|
328
|
+
"-NoProfile",
|
|
329
|
+
"-NonInteractive",
|
|
330
|
+
"-Command",
|
|
331
|
+
`Add-Type -TypeDefinition $env:${WINDOWS_JOB_HELPER_SOURCE_ENV} -Language CSharp -OutputAssembly $env:${WINDOWS_JOB_HELPER_OUTPUT_ENV} -OutputType ConsoleApplication`,
|
|
332
|
+
], {
|
|
333
|
+
env: {
|
|
334
|
+
...process.env,
|
|
335
|
+
[WINDOWS_JOB_HELPER_OUTPUT_ENV]: helperPath,
|
|
336
|
+
[WINDOWS_JOB_HELPER_SOURCE_ENV]: WINDOWS_JOB_HELPER_SOURCE,
|
|
337
|
+
},
|
|
338
|
+
timeout: 15_000,
|
|
339
|
+
windowsHide: true,
|
|
340
|
+
}, signal);
|
|
341
|
+
const probeCommand = windowsShellCommand("exit 0");
|
|
342
|
+
await runWindowsJobHelperCommand(helperPath, [], {
|
|
343
|
+
env: {
|
|
344
|
+
...process.env,
|
|
345
|
+
[WINDOWS_JOB_COMMAND_ENV]: Buffer.from(probeCommand.commandLine).toString("base64"),
|
|
346
|
+
[WINDOWS_JOB_SHELL_ENV]: Buffer.from(probeCommand.shell).toString("base64"),
|
|
347
|
+
},
|
|
348
|
+
timeout: 5_000,
|
|
349
|
+
windowsHide: true,
|
|
350
|
+
}, signal);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
removeWindowsJobHelper(directory);
|
|
354
|
+
throw new Error("Could not initialize the Windows script Job Object helper.", {
|
|
355
|
+
cause: error,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
windowsJobHelperPath = helperPath;
|
|
359
|
+
process.once("exit", () => removeWindowsJobHelper(directory));
|
|
360
|
+
return helperPath;
|
|
361
|
+
}
|
|
362
|
+
function releaseWindowsJobHelperWaiter(bootstrap, reason) {
|
|
363
|
+
bootstrap.waiters -= 1;
|
|
364
|
+
if (bootstrap.waiters !== 0 ||
|
|
365
|
+
windowsJobHelperBootstrap !== bootstrap ||
|
|
366
|
+
windowsJobHelperPath !== undefined) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
windowsJobHelperBootstrap = undefined;
|
|
370
|
+
bootstrap.controller.abort(reason ?? new Error("Windows script helper bootstrap has no active waiters."));
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
function ensureWindowsJobHelper(signal) {
|
|
374
|
+
if (windowsJobHelperPath !== undefined) {
|
|
375
|
+
return Promise.resolve(windowsJobHelperPath);
|
|
376
|
+
}
|
|
377
|
+
if (signal?.aborted) {
|
|
378
|
+
return Promise.reject(signal.reason ?? new Error("Script command aborted."));
|
|
379
|
+
}
|
|
380
|
+
let bootstrap = windowsJobHelperBootstrap;
|
|
381
|
+
if (!bootstrap) {
|
|
382
|
+
const controller = new AbortController();
|
|
383
|
+
bootstrap = {
|
|
384
|
+
controller,
|
|
385
|
+
promise: Promise.resolve(""),
|
|
386
|
+
waiters: 0,
|
|
387
|
+
};
|
|
388
|
+
bootstrap.promise = createWindowsJobHelper(controller.signal).finally(() => {
|
|
389
|
+
if (windowsJobHelperBootstrap === bootstrap) {
|
|
390
|
+
windowsJobHelperBootstrap = undefined;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
windowsJobHelperBootstrap = bootstrap;
|
|
394
|
+
}
|
|
395
|
+
bootstrap.waiters += 1;
|
|
396
|
+
return new Promise((resolve, reject) => {
|
|
397
|
+
let settled = false;
|
|
398
|
+
const finish = (callback) => {
|
|
399
|
+
if (settled) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
settled = true;
|
|
403
|
+
signal?.removeEventListener("abort", abort);
|
|
404
|
+
callback();
|
|
405
|
+
};
|
|
406
|
+
const abort = () => {
|
|
407
|
+
finish(() => {
|
|
408
|
+
const reason = signal?.reason ?? new Error("Script command aborted.");
|
|
409
|
+
const drainsBootstrap = releaseWindowsJobHelperWaiter(bootstrap, reason);
|
|
410
|
+
if (drainsBootstrap) {
|
|
411
|
+
void bootstrap.promise.then(() => reject(reason), () => reject(reason));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
reject(reason);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
419
|
+
void bootstrap.promise.then((value) => {
|
|
420
|
+
finish(() => {
|
|
421
|
+
releaseWindowsJobHelperWaiter(bootstrap);
|
|
422
|
+
resolve(value);
|
|
423
|
+
});
|
|
424
|
+
}, (error) => {
|
|
425
|
+
finish(() => {
|
|
426
|
+
releaseWindowsJobHelperWaiter(bootstrap);
|
|
427
|
+
reject(error);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
if (signal?.aborted) {
|
|
431
|
+
abort();
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function quoteWindowsArgument(value) {
|
|
436
|
+
if (!value) {
|
|
437
|
+
return '""';
|
|
438
|
+
}
|
|
439
|
+
if (!/[\s"]/u.test(value)) {
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
let quoted = '"';
|
|
443
|
+
let backslashes = 0;
|
|
444
|
+
for (const character of value) {
|
|
445
|
+
if (character === "\\") {
|
|
446
|
+
backslashes += 1;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (character === '"') {
|
|
450
|
+
quoted += `${"\\".repeat(backslashes * 2 + 1)}"`;
|
|
451
|
+
backslashes = 0;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
quoted += "\\".repeat(backslashes) + character;
|
|
455
|
+
backslashes = 0;
|
|
456
|
+
}
|
|
457
|
+
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
458
|
+
}
|
|
459
|
+
function windowsShellCommand(command, shell) {
|
|
460
|
+
const resolvedShell = shell ?? process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe";
|
|
461
|
+
const shellCommand = quoteWindowsArgument(resolvedShell);
|
|
462
|
+
const shellName = path.win32.basename(resolvedShell).toLowerCase();
|
|
463
|
+
return {
|
|
464
|
+
commandLine: shellName === "cmd" || shellName === "cmd.exe"
|
|
465
|
+
? `${shellCommand} /d /s /c "${command}"`
|
|
466
|
+
: `${shellCommand} -c ${quoteWindowsArgument(command)}`,
|
|
467
|
+
shell: resolvedShell,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async function spawnScriptChild(params, signal) {
|
|
471
|
+
const cwd = params.cwd ? path.resolve(params.cwd) : process.cwd();
|
|
472
|
+
let startedAtMs;
|
|
473
|
+
let child;
|
|
474
|
+
if (process.platform === "win32") {
|
|
475
|
+
const helperPath = await ensureWindowsJobHelper(signal);
|
|
476
|
+
if (signal?.aborted) {
|
|
477
|
+
throw signal.reason ?? new Error("Script command aborted.");
|
|
478
|
+
}
|
|
479
|
+
startedAtMs = Date.now();
|
|
480
|
+
const shellCommand = windowsShellCommand(params.command, params.shell);
|
|
481
|
+
child = spawn(helperPath, [], {
|
|
482
|
+
cwd,
|
|
483
|
+
env: {
|
|
484
|
+
...process.env,
|
|
485
|
+
[WINDOWS_JOB_COMMAND_ENV]: Buffer.from(shellCommand.commandLine).toString("base64"),
|
|
486
|
+
[WINDOWS_JOB_SHELL_ENV]: Buffer.from(shellCommand.shell).toString("base64"),
|
|
487
|
+
},
|
|
488
|
+
shell: false,
|
|
489
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
490
|
+
windowsHide: true,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
startedAtMs = Date.now();
|
|
495
|
+
child = spawn(params.command, {
|
|
496
|
+
cwd,
|
|
497
|
+
env: process.env,
|
|
498
|
+
shell: params.shell ?? true,
|
|
499
|
+
detached: true,
|
|
500
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
const observedAtMs = Date.now();
|
|
504
|
+
if (signal?.aborted) {
|
|
505
|
+
await terminateChild(child, startedAtMs, observedAtMs);
|
|
506
|
+
throw signal.reason ?? new Error("Script command aborted.");
|
|
507
|
+
}
|
|
508
|
+
return { child, observedAtMs, startedAtMs };
|
|
509
|
+
}
|
|
8
510
|
const ScriptMessageSchema = z.object({
|
|
9
511
|
author: z.enum(["assistant", "system", "user"]),
|
|
10
512
|
id: z.string().min(1),
|
|
@@ -29,56 +531,196 @@ const ScriptInboundResultSchema = z
|
|
|
29
531
|
})
|
|
30
532
|
.refine((result) => (result.timeout === true && result.message === undefined) ||
|
|
31
533
|
(result.timeout !== true && result.message !== undefined), { message: "result must contain either a message or timeout: true" });
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
534
|
+
function runTerminationCommand(command, args) {
|
|
535
|
+
return new Promise((resolve) => {
|
|
536
|
+
let cleanup;
|
|
537
|
+
try {
|
|
538
|
+
cleanup = spawn(command, args, {
|
|
539
|
+
stdio: "ignore",
|
|
540
|
+
windowsHide: true,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
resolve(false);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
let settled = false;
|
|
548
|
+
const finish = (succeeded) => {
|
|
549
|
+
if (settled) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
settled = true;
|
|
553
|
+
clearTimeout(timeout);
|
|
554
|
+
resolve(succeeded);
|
|
555
|
+
};
|
|
556
|
+
const timeout = setTimeout(() => {
|
|
36
557
|
try {
|
|
37
|
-
|
|
38
|
-
stdio: "ignore",
|
|
39
|
-
windowsHide: true,
|
|
40
|
-
});
|
|
558
|
+
cleanup.kill("SIGKILL");
|
|
41
559
|
}
|
|
42
560
|
catch {
|
|
43
|
-
|
|
44
|
-
return;
|
|
561
|
+
// The cleanup process may have exited at the timeout boundary.
|
|
45
562
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
|
|
563
|
+
finish(false);
|
|
564
|
+
}, WINDOWS_TERMINATION_COMMAND_TIMEOUT_MS);
|
|
565
|
+
timeout.unref();
|
|
566
|
+
cleanup.once("close", (code) => finish(code === 0));
|
|
567
|
+
cleanup.once("error", () => finish(false));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function windowsProcessTreeTermination(pid, childStartedAtMs, childObservedAtMs, rootExpectedAlive) {
|
|
571
|
+
const rootNotBeforeMs = Math.max(0, Math.floor(childStartedAtMs));
|
|
572
|
+
const rootObservedByMs = Math.max(rootNotBeforeMs, Math.ceil(childObservedAtMs) + 1);
|
|
573
|
+
return [
|
|
574
|
+
"$ErrorActionPreference='Stop'",
|
|
575
|
+
`$NativeSource='${WINDOWS_PROCESS_TERMINATOR_SOURCE}'`,
|
|
576
|
+
"Add-Type -TypeDefinition $NativeSource",
|
|
577
|
+
`$RootProcessId=${pid}`,
|
|
578
|
+
`$RootExpectedAlive=$${rootExpectedAlive ? "true" : "false"}`,
|
|
579
|
+
`$RootNotBefore=[DateTimeOffset]::FromUnixTimeMilliseconds(${rootNotBeforeMs}).UtcDateTime`,
|
|
580
|
+
`$RootObservedBy=[DateTimeOffset]::FromUnixTimeMilliseconds(${rootObservedByMs}).UtcDateTime`,
|
|
581
|
+
"$AllProcesses=@(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate)",
|
|
582
|
+
"$Snapshot=[System.Collections.Generic.List[object]]::new()",
|
|
583
|
+
"$Pending=[System.Collections.Generic.Queue[object]]::new()",
|
|
584
|
+
"$Visited=[System.Collections.Generic.HashSet[string]]::new()",
|
|
585
|
+
"$RootProcess=$AllProcesses | Where-Object { [int]$_.ProcessId -eq $RootProcessId } | Select-Object -First 1",
|
|
586
|
+
"$RootCreated=if($null -ne $RootProcess){([datetime]$RootProcess.CreationDate).ToUniversalTime()}else{$null}",
|
|
587
|
+
"$RootMatches=$null -ne $RootProcess -and $RootCreated -ge $RootNotBefore -and $RootCreated -le $RootObservedBy",
|
|
588
|
+
"$KillRoot=$RootExpectedAlive -and $RootMatches",
|
|
589
|
+
"$CleanupFailed=$RootExpectedAlive -and !$RootMatches",
|
|
590
|
+
"if($KillRoot){",
|
|
591
|
+
"$Snapshot.Add($RootProcess)",
|
|
592
|
+
'$Visited.Add("$([int]$RootProcess.ProcessId)|$(([datetime]$RootProcess.CreationDate).ToFileTimeUtc())") | Out-Null',
|
|
593
|
+
"$Pending.Enqueue($RootProcess)",
|
|
594
|
+
"}",
|
|
595
|
+
"while($Pending.Count -gt 0){",
|
|
596
|
+
"$Parent=$Pending.Dequeue()",
|
|
597
|
+
"foreach($Process in @($AllProcesses | Where-Object { [int]$_.ParentProcessId -eq [int]$Parent.ProcessId })){",
|
|
598
|
+
"$ChildCreated=([datetime]$Process.CreationDate).ToUniversalTime()",
|
|
599
|
+
"$ParentCreated=([datetime]$Parent.CreationDate).ToUniversalTime()",
|
|
600
|
+
"if($ChildCreated -lt $ParentCreated){continue}",
|
|
601
|
+
'$Identity="$([int]$Process.ProcessId)|$($ChildCreated.ToFileTimeUtc())"',
|
|
602
|
+
"if(!$Visited.Add($Identity)){continue}",
|
|
603
|
+
"$Snapshot.Add($Process)",
|
|
604
|
+
"$Pending.Enqueue($Process)",
|
|
605
|
+
"}",
|
|
606
|
+
"}",
|
|
607
|
+
"if($KillRoot){",
|
|
608
|
+
"$Entries=@($Snapshot)",
|
|
609
|
+
"[array]::Reverse($Entries)",
|
|
610
|
+
"foreach($Entry in $Entries){",
|
|
611
|
+
"$CreationTime=([datetime]$Entry.CreationDate).ToFileTimeUtc()",
|
|
612
|
+
"if(![CrablineProcessTerminator]::TerminateVerified([int]$Entry.ProcessId,$CreationTime)){$CleanupFailed=$true}",
|
|
613
|
+
"}",
|
|
614
|
+
"}",
|
|
615
|
+
"if($CleanupFailed){exit 1}",
|
|
616
|
+
].join(";");
|
|
617
|
+
}
|
|
618
|
+
function destroyChildPipes(child) {
|
|
619
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) {
|
|
65
620
|
try {
|
|
66
|
-
|
|
621
|
+
stream?.destroy();
|
|
67
622
|
}
|
|
68
623
|
catch {
|
|
69
|
-
//
|
|
624
|
+
// Pipe teardown is best effort after process-tree termination.
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function isChildRunning(child) {
|
|
629
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
return child.kill(0);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function terminateChild(child, childStartedAtMs, childObservedAtMs) {
|
|
640
|
+
try {
|
|
641
|
+
const childRunning = isChildRunning(child);
|
|
642
|
+
if (process.platform === "win32") {
|
|
643
|
+
if (child.pid) {
|
|
644
|
+
const treeTerminated = await runTerminationCommand("powershell.exe", [
|
|
645
|
+
"-NoLogo",
|
|
646
|
+
"-NoProfile",
|
|
647
|
+
"-NonInteractive",
|
|
648
|
+
"-Command",
|
|
649
|
+
windowsProcessTreeTermination(child.pid, childStartedAtMs, childObservedAtMs, childRunning),
|
|
650
|
+
]);
|
|
651
|
+
if (!treeTerminated && isChildRunning(child)) {
|
|
652
|
+
try {
|
|
653
|
+
child.kill("SIGKILL");
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// Closing the helper handle is the bounded fallback for its Job Object.
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else if (child.pid) {
|
|
662
|
+
try {
|
|
663
|
+
process.kill(-child.pid, "SIGKILL");
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
// The process group may have exited with the shell.
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (isChildRunning(child)) {
|
|
670
|
+
try {
|
|
671
|
+
child.kill("SIGKILL");
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
// The process may have exited while its descendants retained the pipes.
|
|
675
|
+
}
|
|
70
676
|
}
|
|
71
677
|
}
|
|
72
|
-
|
|
73
|
-
child
|
|
678
|
+
finally {
|
|
679
|
+
destroyChildPipes(child);
|
|
74
680
|
}
|
|
75
681
|
}
|
|
682
|
+
async function waitForChildClose(childClosed) {
|
|
683
|
+
let timeout;
|
|
684
|
+
const exit = await Promise.race([
|
|
685
|
+
childClosed,
|
|
686
|
+
new Promise((resolve) => {
|
|
687
|
+
timeout = setTimeout(() => resolve(undefined), CHILD_CLOSE_TIMEOUT_MS);
|
|
688
|
+
timeout.unref();
|
|
689
|
+
}),
|
|
690
|
+
]);
|
|
691
|
+
clearTimeout(timeout);
|
|
692
|
+
return exit;
|
|
693
|
+
}
|
|
694
|
+
function waitForChildCloseOrAbort(childClosed, signals) {
|
|
695
|
+
const activeSignals = signals.filter((signal) => signal !== undefined);
|
|
696
|
+
if (activeSignals.some((signal) => signal.aborted)) {
|
|
697
|
+
return Promise.resolve(undefined);
|
|
698
|
+
}
|
|
699
|
+
return new Promise((resolve) => {
|
|
700
|
+
let settled = false;
|
|
701
|
+
const abort = () => finish(undefined);
|
|
702
|
+
const finish = (exit) => {
|
|
703
|
+
if (settled) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
settled = true;
|
|
707
|
+
for (const signal of activeSignals) {
|
|
708
|
+
signal.removeEventListener("abort", abort);
|
|
709
|
+
}
|
|
710
|
+
resolve(exit);
|
|
711
|
+
};
|
|
712
|
+
for (const signal of activeSignals) {
|
|
713
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
714
|
+
}
|
|
715
|
+
void childClosed.then((exit) => finish(exit));
|
|
716
|
+
});
|
|
717
|
+
}
|
|
76
718
|
function formatValidationError(error) {
|
|
77
719
|
return error.issues
|
|
78
720
|
.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "result"}: ${issue.message}`)
|
|
79
721
|
.join("; ");
|
|
80
722
|
}
|
|
81
|
-
const sensitiveEnvironmentNameFragmentPattern = /(?:AUTH|BEARER|CREDENTIAL|KEY|PASS|PRIVATE|SECRET|TOKEN)/iu;
|
|
723
|
+
const sensitiveEnvironmentNameFragmentPattern = /(?:AUTH|BEARER|CREDENTIAL|JWT|KEY|PASS|PRIVATE|SECRET|TOKEN)/iu;
|
|
82
724
|
const nonSensitiveWorkingDirectoryNames = new Set(["PWD", "OLDPWD"]);
|
|
83
725
|
function isSensitiveEnvironmentName(name) {
|
|
84
726
|
const upperName = name.toUpperCase();
|
|
@@ -86,7 +728,20 @@ function isSensitiveEnvironmentName(name) {
|
|
|
86
728
|
(!nonSensitiveWorkingDirectoryNames.has(upperName) && upperName.includes("PWD")) ||
|
|
87
729
|
/PAT(?!H)/u.test(upperName));
|
|
88
730
|
}
|
|
731
|
+
function redactCredentialSyntax(detail) {
|
|
732
|
+
let redacted = detail.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^/\s@]+)@/giu, "$1[redacted credentials]@");
|
|
733
|
+
redacted = redacted.replace(/(\bauthorization\b["']?\s*[:=]\s*["']?\s*)(?:(basic|bearer)\s+)?([^\s"',;}]+)/giu, (_match, prefix, scheme) => `${prefix}${scheme ? `${scheme} ` : ""}[redacted credential]`);
|
|
734
|
+
redacted = redacted.replace(/\b([A-Za-z_][A-Za-z0-9_-]*)\s*(\+?=|:)\s*("[^"]*"|'[^']*'|[^,;}\]\r\n]+)/gu, (match, name, operator) => name.toLowerCase() !== "authorization" && isSensitiveEnvironmentName(name)
|
|
735
|
+
? `${name}${operator}[redacted credential]`
|
|
736
|
+
: match);
|
|
737
|
+
return redacted.replace(/--([A-Za-z0-9][A-Za-z0-9_-]*)(=|\s+)("[^"]*"|'[^']*'|[^\s,;]+)/gu, (match, name, separator) => isSensitiveEnvironmentName(name.replaceAll("-", "_"))
|
|
738
|
+
? `--${name}${separator}[redacted credential]`
|
|
739
|
+
: match);
|
|
740
|
+
}
|
|
89
741
|
function commandContainsSensitiveValue(command) {
|
|
742
|
+
if (redactCredentialSyntax(command) !== command) {
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
90
745
|
const assignments = [
|
|
91
746
|
...command.matchAll(/(?:^|[\s;&|])["']?([A-Za-z_][A-Za-z0-9_]*)\s*\+?=/gu),
|
|
92
747
|
...command.matchAll(/\$env:([A-Za-z_][A-Za-z0-9_]*)\s*\+?=/giu),
|
|
@@ -97,6 +752,147 @@ function commandContainsSensitiveValue(command) {
|
|
|
97
752
|
}
|
|
98
753
|
return [...command.matchAll(/(?:^|[\s;&|])["']?--([A-Za-z0-9][A-Za-z0-9_-]*)(?:=|\s)/gu)].some((match) => isSensitiveEnvironmentName((match[1] ?? "").replaceAll("-", "_")));
|
|
99
754
|
}
|
|
755
|
+
function addRedactionRepresentations(values, value) {
|
|
756
|
+
if (!value) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
values.add(value);
|
|
760
|
+
const serialized = JSON.stringify(value);
|
|
761
|
+
values.add(serialized);
|
|
762
|
+
values.add(serialized.slice(1, -1));
|
|
763
|
+
}
|
|
764
|
+
function addCommandValueRedactions(substringValues, value) {
|
|
765
|
+
addRedactionRepresentations(substringValues, value);
|
|
766
|
+
}
|
|
767
|
+
function tokenizeLiteralCommand(command) {
|
|
768
|
+
const tokens = [];
|
|
769
|
+
let current = "";
|
|
770
|
+
let quote;
|
|
771
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
772
|
+
const character = command[index];
|
|
773
|
+
if (character === "\n" || character === "\r") {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
if (process.platform === "win32" && character === "\\" && command[index + 1] === '"') {
|
|
777
|
+
return undefined;
|
|
778
|
+
}
|
|
779
|
+
if (process.platform === "win32" && character === '"' && command[index + 1] === '"') {
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
if (quote) {
|
|
783
|
+
if (character === quote) {
|
|
784
|
+
quote = undefined;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (quote === '"' && /[$`%!]/u.test(character)) {
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
if (character === "\\" && quote === '"' && process.platform !== "win32") {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
current += character;
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (character === '"' || (character === "'" && process.platform !== "win32")) {
|
|
797
|
+
quote = character;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (/\s/u.test(character)) {
|
|
801
|
+
if (current) {
|
|
802
|
+
tokens.push(current);
|
|
803
|
+
current = "";
|
|
804
|
+
}
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (/[;&|<>(){}$`%!*?[\]~]/u.test(character) ||
|
|
808
|
+
(process.platform === "win32" && character === "^")) {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
if (character === "\\") {
|
|
812
|
+
if (process.platform !== "win32") {
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
current += character;
|
|
817
|
+
}
|
|
818
|
+
if (quote) {
|
|
819
|
+
return undefined;
|
|
820
|
+
}
|
|
821
|
+
if (current) {
|
|
822
|
+
tokens.push(current);
|
|
823
|
+
}
|
|
824
|
+
return tokens;
|
|
825
|
+
}
|
|
826
|
+
function snapshotCommandValues(command) {
|
|
827
|
+
const tokens = tokenizeLiteralCommand(command);
|
|
828
|
+
if (!tokens) {
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
const exactValues = new Set();
|
|
832
|
+
const substringValues = new Set();
|
|
833
|
+
let executableSeen = false;
|
|
834
|
+
for (const token of tokens) {
|
|
835
|
+
if (!executableSeen) {
|
|
836
|
+
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)\+?=(.*)$/u.exec(token);
|
|
837
|
+
if (assignment) {
|
|
838
|
+
addCommandValueRedactions(substringValues, assignment[2] ?? "");
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
executableSeen = true;
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (/^--[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(token)) {
|
|
845
|
+
const value = token.slice(2);
|
|
846
|
+
if (value.length < 3) {
|
|
847
|
+
return undefined;
|
|
848
|
+
}
|
|
849
|
+
addRedactionRepresentations(exactValues, token);
|
|
850
|
+
addCommandValueRedactions(substringValues, value);
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (/^-[A-Za-z0-9][A-Za-z0-9_-]+$/u.test(token)) {
|
|
854
|
+
const value = token.slice(2);
|
|
855
|
+
if (value.length < 3) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
addRedactionRepresentations(exactValues, token);
|
|
859
|
+
addCommandValueRedactions(substringValues, token.slice(1));
|
|
860
|
+
addCommandValueRedactions(substringValues, value);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const option = /^--?[A-Za-z0-9][A-Za-z0-9_-]*=(.*)$/u.exec(token);
|
|
864
|
+
if (option) {
|
|
865
|
+
const value = option[1] ?? "";
|
|
866
|
+
if (value.length < 3) {
|
|
867
|
+
return undefined;
|
|
868
|
+
}
|
|
869
|
+
addCommandValueRedactions(substringValues, value);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (process.platform === "win32" && token.startsWith("/")) {
|
|
873
|
+
const slashOption = /^\/[A-Za-z0-9][A-Za-z0-9_-]*[:=](.*)$/u.exec(token);
|
|
874
|
+
const value = slashOption?.[1] ?? "";
|
|
875
|
+
if (value.length < 3) {
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
addCommandValueRedactions(substringValues, value);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (token.length < 3) {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
if (token.startsWith("-")) {
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
addRedactionRepresentations(substringValues, token);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
exactValues: [...exactValues].sort((left, right) => right.length - left.length),
|
|
893
|
+
substringValues: [...substringValues].sort((left, right) => right.length - left.length),
|
|
894
|
+
};
|
|
895
|
+
}
|
|
100
896
|
function snapshotSensitiveEnvironmentValues() {
|
|
101
897
|
const representations = new Set();
|
|
102
898
|
const values = Object.entries(process.env)
|
|
@@ -104,19 +900,10 @@ function snapshotSensitiveEnvironmentValues() {
|
|
|
104
900
|
.map(([, value]) => value)
|
|
105
901
|
.sort((left, right) => right.length - left.length);
|
|
106
902
|
for (const value of values) {
|
|
107
|
-
representations
|
|
108
|
-
representations.add(JSON.stringify(value));
|
|
109
|
-
representations.add(JSON.stringify(value).slice(1, -1));
|
|
903
|
+
addRedactionRepresentations(representations, value);
|
|
110
904
|
}
|
|
111
905
|
return [...representations].sort((left, right) => right.length - left.length);
|
|
112
906
|
}
|
|
113
|
-
function redactSensitiveEnvironmentValues(detail, sensitiveEnvironmentValues) {
|
|
114
|
-
let redacted = detail;
|
|
115
|
-
for (const representation of sensitiveEnvironmentValues) {
|
|
116
|
-
redacted = redacted.split(representation).join("[redacted environment value]");
|
|
117
|
-
}
|
|
118
|
-
return redacted;
|
|
119
|
-
}
|
|
120
907
|
function collectSensitivePayloadValues(value, values, seen, sensitive = false) {
|
|
121
908
|
if (typeof value === "string") {
|
|
122
909
|
if (sensitive && value.length > 0) {
|
|
@@ -157,10 +944,54 @@ function snapshotSensitivePayloadValues(payload) {
|
|
|
157
944
|
});
|
|
158
945
|
return [...values].sort((left, right) => right.length - left.length);
|
|
159
946
|
}
|
|
160
|
-
function
|
|
947
|
+
function isExactCommandValueBoundary(character) {
|
|
948
|
+
return character === undefined || /[\s"'`=,:;.()[\]{}<>!?]/u.test(character);
|
|
949
|
+
}
|
|
950
|
+
function collectDiagnosticRedactionSpans(detail, values, label, priority, exact = false) {
|
|
951
|
+
const spans = [];
|
|
952
|
+
for (const value of values) {
|
|
953
|
+
let cursor = 0;
|
|
954
|
+
while (cursor < detail.length) {
|
|
955
|
+
const start = detail.indexOf(value, cursor);
|
|
956
|
+
if (start < 0) {
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
const end = start + value.length;
|
|
960
|
+
if (!exact ||
|
|
961
|
+
(isExactCommandValueBoundary(detail[start - 1]) && isExactCommandValueBoundary(detail[end]))) {
|
|
962
|
+
spans.push({ end, label, priority, sourceLength: value.length, start });
|
|
963
|
+
}
|
|
964
|
+
cursor = end;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return spans;
|
|
968
|
+
}
|
|
969
|
+
function redactDiagnosticValues(detail, diagnostics) {
|
|
970
|
+
const spans = [
|
|
971
|
+
...collectDiagnosticRedactionSpans(detail, diagnostics.sensitiveEnvironmentValues, "[redacted environment value]", 4),
|
|
972
|
+
...collectDiagnosticRedactionSpans(detail, diagnostics.configuredCommands, "[configured script command]", 3),
|
|
973
|
+
...collectDiagnosticRedactionSpans(detail, diagnostics.commandValues, "[redacted command value]", 3),
|
|
974
|
+
...collectDiagnosticRedactionSpans(detail, diagnostics.exactCommandValues, "[redacted command value]", 3, true),
|
|
975
|
+
...collectDiagnosticRedactionSpans(detail, diagnostics.sensitivePayloadValues, "[redacted configured value]", 2),
|
|
976
|
+
].sort((left, right) => left.start - right.start || right.end - left.end || right.priority - left.priority);
|
|
977
|
+
const merged = [];
|
|
978
|
+
for (const span of spans) {
|
|
979
|
+
const previous = merged.at(-1);
|
|
980
|
+
if (!previous || span.start >= previous.end) {
|
|
981
|
+
merged.push({ ...span });
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
previous.end = Math.max(previous.end, span.end);
|
|
985
|
+
if (span.sourceLength > previous.sourceLength ||
|
|
986
|
+
(span.sourceLength === previous.sourceLength && span.priority > previous.priority)) {
|
|
987
|
+
previous.label = span.label;
|
|
988
|
+
previous.priority = span.priority;
|
|
989
|
+
previous.sourceLength = span.sourceLength;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
161
992
|
let redacted = detail;
|
|
162
|
-
for (const
|
|
163
|
-
redacted = redacted.
|
|
993
|
+
for (const span of merged.toReversed()) {
|
|
994
|
+
redacted = redacted.slice(0, span.start) + span.label + redacted.slice(span.end);
|
|
164
995
|
}
|
|
165
996
|
return redacted;
|
|
166
997
|
}
|
|
@@ -168,17 +999,25 @@ function formatScriptError(summary, detail, command, diagnostics) {
|
|
|
168
999
|
if (!detail.trim()) {
|
|
169
1000
|
return summary;
|
|
170
1001
|
}
|
|
171
|
-
if (commandContainsSensitiveValue(command)) {
|
|
1002
|
+
if (!diagnostics.diagnosticsSafe || commandContainsSensitiveValue(command)) {
|
|
172
1003
|
return `${summary}\n[script diagnostics redacted]`;
|
|
173
1004
|
}
|
|
174
|
-
let redacted =
|
|
175
|
-
|
|
176
|
-
redacted = redacted.split(configuredCommand).join("[configured script command]");
|
|
177
|
-
}
|
|
1005
|
+
let redacted = redactDiagnosticValues(detail, diagnostics);
|
|
1006
|
+
redacted = redactCredentialSyntax(redacted);
|
|
178
1007
|
redacted = redacted.trim();
|
|
179
1008
|
return redacted ? `${summary}\n${redacted}` : summary;
|
|
180
1009
|
}
|
|
181
|
-
function
|
|
1010
|
+
function usesSupportedDiagnosticShell(shell) {
|
|
1011
|
+
if (shell !== undefined) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
if (process.platform !== "win32") {
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
const comspec = process.env.ComSpec ?? process.env.COMSPEC;
|
|
1018
|
+
return comspec === undefined || /(?:^|[\\/])cmd(?:\.exe)?$/iu.test(comspec);
|
|
1019
|
+
}
|
|
1020
|
+
function createScriptDiagnosticsSnapshot(command, serializedPayload, shell) {
|
|
182
1021
|
const payload = JSON.parse(serializedPayload);
|
|
183
1022
|
const configuredCommands = new Set([command]);
|
|
184
1023
|
const commands = payload.provider?.config?.script?.commands;
|
|
@@ -187,8 +1026,30 @@ function createScriptDiagnosticsSnapshot(command, serializedPayload) {
|
|
|
187
1026
|
configuredCommands.add(configuredCommand);
|
|
188
1027
|
}
|
|
189
1028
|
}
|
|
1029
|
+
const commandValues = new Set();
|
|
1030
|
+
const exactCommandValues = new Set();
|
|
1031
|
+
let diagnosticsSafe = usesSupportedDiagnosticShell(shell);
|
|
1032
|
+
for (const configuredCommand of configuredCommands) {
|
|
1033
|
+
if (commandContainsSensitiveValue(configuredCommand)) {
|
|
1034
|
+
diagnosticsSafe = false;
|
|
1035
|
+
}
|
|
1036
|
+
const values = snapshotCommandValues(configuredCommand);
|
|
1037
|
+
if (!values) {
|
|
1038
|
+
diagnosticsSafe = false;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
for (const value of values.substringValues) {
|
|
1042
|
+
commandValues.add(value);
|
|
1043
|
+
}
|
|
1044
|
+
for (const value of values.exactValues) {
|
|
1045
|
+
exactCommandValues.add(value);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
190
1048
|
return {
|
|
1049
|
+
commandValues: [...commandValues].sort((left, right) => right.length - left.length),
|
|
191
1050
|
configuredCommands: [...configuredCommands].sort((left, right) => right.length - left.length),
|
|
1051
|
+
diagnosticsSafe,
|
|
1052
|
+
exactCommandValues: [...exactCommandValues].sort((left, right) => right.length - left.length),
|
|
192
1053
|
sensitiveEnvironmentValues: snapshotSensitiveEnvironmentValues(),
|
|
193
1054
|
sensitivePayloadValues: snapshotSensitivePayloadValues(payload),
|
|
194
1055
|
};
|
|
@@ -212,34 +1073,20 @@ function runScript(params) {
|
|
|
212
1073
|
return Promise.reject(params.signal.reason ?? new Error("Script command aborted."));
|
|
213
1074
|
}
|
|
214
1075
|
const serializedPayload = JSON.stringify(params.payload);
|
|
215
|
-
const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload);
|
|
1076
|
+
const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload, params.shell);
|
|
216
1077
|
return new Promise((resolve, reject) => {
|
|
217
1078
|
let child;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
cwd: params.cwd ? path.resolve(params.cwd) : process.cwd(),
|
|
221
|
-
env: process.env,
|
|
222
|
-
shell: params.shell ?? true,
|
|
223
|
-
detached: process.platform !== "win32",
|
|
224
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
228
|
-
reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
1079
|
+
let childStartedAtMs = 0;
|
|
1080
|
+
let childObservedAtMs = 0;
|
|
231
1081
|
const stdout = [];
|
|
232
1082
|
const stderr = [];
|
|
233
1083
|
let deadlineExceeded = false;
|
|
234
1084
|
let outputBytes = 0;
|
|
235
1085
|
let settled = false;
|
|
1086
|
+
let timeout;
|
|
236
1087
|
let timeoutGrace;
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
await terminateChild(child);
|
|
240
|
-
reject(params.signal?.reason ?? new Error("Script command aborted."));
|
|
241
|
-
});
|
|
242
|
-
};
|
|
1088
|
+
const spawnAbortController = new AbortController();
|
|
1089
|
+
let spawnCompletion;
|
|
243
1090
|
const finish = (callback) => {
|
|
244
1091
|
if (settled) {
|
|
245
1092
|
return;
|
|
@@ -247,80 +1094,98 @@ function runScript(params) {
|
|
|
247
1094
|
settled = true;
|
|
248
1095
|
clearTimeout(timeout);
|
|
249
1096
|
clearTimeout(timeoutGrace);
|
|
1097
|
+
spawnAbortController.abort();
|
|
250
1098
|
params.signal?.removeEventListener("abort", abort);
|
|
251
1099
|
void callback();
|
|
252
1100
|
};
|
|
1101
|
+
const stopChild = async () => {
|
|
1102
|
+
if (child) {
|
|
1103
|
+
await terminateChild(child, childStartedAtMs, childObservedAtMs);
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
await spawnCompletion;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
const abort = () => {
|
|
1110
|
+
finish(async () => {
|
|
1111
|
+
await stopChild();
|
|
1112
|
+
reject(params.signal?.reason ?? new Error("Script command aborted."));
|
|
1113
|
+
});
|
|
1114
|
+
};
|
|
253
1115
|
const failForOutputLimit = () => {
|
|
254
1116
|
finish(async () => {
|
|
255
|
-
await
|
|
1117
|
+
await stopChild();
|
|
256
1118
|
reject(new CrablineError(`Script command exceeded ${MAX_SCRIPT_OUTPUT_BYTES} bytes of output.`, {
|
|
257
1119
|
kind: "connectivity",
|
|
258
1120
|
}));
|
|
259
1121
|
});
|
|
260
1122
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
child.stderr.on("data", (chunk) => {
|
|
271
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
272
|
-
outputBytes += buffer.length;
|
|
273
|
-
if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
274
|
-
failForOutputLimit();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
stderr.push(buffer);
|
|
278
|
-
});
|
|
279
|
-
child.stdin.on("error", () => {
|
|
280
|
-
// Child closure is reported through the process error/close handlers.
|
|
281
|
-
});
|
|
282
|
-
child.once("error", (error) => {
|
|
283
|
-
finish(() => {
|
|
284
|
-
reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
|
|
1123
|
+
const startChild = (spawnedChild) => {
|
|
1124
|
+
spawnedChild.stdout.on("data", (chunk) => {
|
|
1125
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1126
|
+
outputBytes += buffer.length;
|
|
1127
|
+
if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
1128
|
+
failForOutputLimit();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
stdout.push(buffer);
|
|
285
1132
|
});
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (code !== 0) {
|
|
292
|
-
reject(new CrablineError(formatScriptError(`Script command failed${signal ? ` (${signal})` : ""}.`, stderrText.trim() ? stderrText : stdoutText, params.command, diagnostics), { kind: "connectivity" }));
|
|
1133
|
+
spawnedChild.stderr.on("data", (chunk) => {
|
|
1134
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1135
|
+
outputBytes += buffer.length;
|
|
1136
|
+
if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
1137
|
+
failForOutputLimit();
|
|
293
1138
|
return;
|
|
294
1139
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1140
|
+
stderr.push(buffer);
|
|
1141
|
+
});
|
|
1142
|
+
spawnedChild.stdin.on("error", () => {
|
|
1143
|
+
// Child closure is reported through the process error/close handlers.
|
|
1144
|
+
});
|
|
1145
|
+
spawnedChild.once("error", (error) => {
|
|
1146
|
+
finish(() => {
|
|
1147
|
+
reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
spawnedChild.once("close", (code, signal) => {
|
|
1151
|
+
finish(() => {
|
|
1152
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
1153
|
+
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
1154
|
+
if (code !== 0) {
|
|
1155
|
+
reject(new CrablineError(formatScriptError(`Script command failed${signal ? ` (${signal})` : ""}.`, stderrText.trim() ? stderrText : stdoutText, params.command, diagnostics), { kind: "connectivity" }));
|
|
306
1156
|
return;
|
|
307
1157
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1158
|
+
try {
|
|
1159
|
+
const result = parseScriptJson({
|
|
1160
|
+
command: params.command,
|
|
1161
|
+
diagnostics,
|
|
1162
|
+
output: stdoutText,
|
|
1163
|
+
schema: params.schema,
|
|
1164
|
+
});
|
|
1165
|
+
if (deadlineExceeded && !params.acceptResultDuringTimeoutGrace?.(result)) {
|
|
1166
|
+
reject(new CrablineError(`Script command timed out after ${params.timeoutMs}ms.`, {
|
|
1167
|
+
kind: "timeout",
|
|
1168
|
+
}));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
resolve(result);
|
|
1172
|
+
}
|
|
1173
|
+
catch (error) {
|
|
1174
|
+
reject(error);
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
313
1177
|
});
|
|
314
|
-
|
|
1178
|
+
spawnedChild.stdin.end(serializedPayload);
|
|
1179
|
+
};
|
|
315
1180
|
const failForTimeout = () => {
|
|
316
1181
|
finish(async () => {
|
|
317
|
-
await
|
|
1182
|
+
await stopChild();
|
|
318
1183
|
reject(new CrablineError(`Script command timed out after ${params.timeoutMs}ms.`, {
|
|
319
1184
|
kind: "timeout",
|
|
320
1185
|
}));
|
|
321
1186
|
});
|
|
322
1187
|
};
|
|
323
|
-
|
|
1188
|
+
timeout = setTimeout(() => {
|
|
324
1189
|
const timeoutGraceMs = params.timeoutGraceMs ?? 0;
|
|
325
1190
|
if (timeoutGraceMs <= 0) {
|
|
326
1191
|
failForTimeout();
|
|
@@ -331,12 +1196,28 @@ function runScript(params) {
|
|
|
331
1196
|
timeoutGrace.unref();
|
|
332
1197
|
}, params.timeoutMs);
|
|
333
1198
|
timeout.unref();
|
|
1199
|
+
params.signal?.addEventListener("abort", abort, { once: true });
|
|
334
1200
|
if (params.signal?.aborted) {
|
|
335
1201
|
abort();
|
|
336
1202
|
return;
|
|
337
1203
|
}
|
|
338
|
-
params.signal
|
|
339
|
-
|
|
1204
|
+
spawnCompletion = spawnScriptChild(params, spawnAbortController.signal).then(async (spawned) => {
|
|
1205
|
+
if (settled) {
|
|
1206
|
+
await terminateChild(spawned.child, spawned.startedAtMs, spawned.observedAtMs);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
child = spawned.child;
|
|
1210
|
+
childStartedAtMs = spawned.startedAtMs;
|
|
1211
|
+
childObservedAtMs = spawned.observedAtMs;
|
|
1212
|
+
startChild(spawned.child);
|
|
1213
|
+
}, (error) => {
|
|
1214
|
+
if (settled) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
finish(() => {
|
|
1218
|
+
reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
340
1221
|
});
|
|
341
1222
|
}
|
|
342
1223
|
function watchScript(params) {
|
|
@@ -355,31 +1236,45 @@ function watchScript(params) {
|
|
|
355
1236
|
},
|
|
356
1237
|
};
|
|
357
1238
|
const serializedPayload = JSON.stringify(payload);
|
|
358
|
-
const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload);
|
|
1239
|
+
const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload, params.shell);
|
|
359
1240
|
let child;
|
|
1241
|
+
let childStartedAtMs;
|
|
1242
|
+
let childObservedAtMs;
|
|
1243
|
+
const spawnSignal = params.context.signal
|
|
1244
|
+
? AbortSignal.any([params.cancelSignal, params.context.signal])
|
|
1245
|
+
: params.cancelSignal;
|
|
360
1246
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
367
|
-
});
|
|
1247
|
+
({
|
|
1248
|
+
child,
|
|
1249
|
+
observedAtMs: childObservedAtMs,
|
|
1250
|
+
startedAtMs: childStartedAtMs,
|
|
1251
|
+
} = await spawnScriptChild(params, spawnSignal));
|
|
368
1252
|
}
|
|
369
1253
|
catch (error) {
|
|
1254
|
+
if (params.cancelSignal.aborted) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (params.context.signal?.aborted) {
|
|
1258
|
+
throw params.context.signal.reason ?? new Error("Script watch command aborted.");
|
|
1259
|
+
}
|
|
370
1260
|
throw new CrablineError(formatScriptError("Script watch command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" });
|
|
371
1261
|
}
|
|
372
1262
|
let buffer = "";
|
|
373
1263
|
let stderr = "";
|
|
374
1264
|
let childError;
|
|
375
1265
|
let outputLimitError;
|
|
1266
|
+
let termination;
|
|
376
1267
|
const stopChild = () => {
|
|
377
|
-
|
|
1268
|
+
termination ??= terminateChild(child, childStartedAtMs, childObservedAtMs);
|
|
1269
|
+
return termination;
|
|
378
1270
|
};
|
|
379
|
-
|
|
380
|
-
|
|
1271
|
+
const requestStopChild = () => {
|
|
1272
|
+
void stopChild();
|
|
1273
|
+
};
|
|
1274
|
+
params.cancelSignal.addEventListener("abort", requestStopChild, { once: true });
|
|
1275
|
+
params.context.signal?.addEventListener("abort", requestStopChild, { once: true });
|
|
381
1276
|
if (params.cancelSignal.aborted || params.context.signal?.aborted) {
|
|
382
|
-
|
|
1277
|
+
requestStopChild();
|
|
383
1278
|
}
|
|
384
1279
|
child.stdout.setEncoding("utf8");
|
|
385
1280
|
child.stderr.setEncoding("utf8");
|
|
@@ -393,7 +1288,7 @@ function watchScript(params) {
|
|
|
393
1288
|
stderr += chunk;
|
|
394
1289
|
if (Buffer.byteLength(stderr) > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
395
1290
|
outputLimitError = new CrablineError(`Script watch command exceeded ${MAX_SCRIPT_OUTPUT_BYTES} bytes of stderr.`, { kind: "connectivity" });
|
|
396
|
-
|
|
1291
|
+
requestStopChild();
|
|
397
1292
|
}
|
|
398
1293
|
});
|
|
399
1294
|
child.once("error", (error) => {
|
|
@@ -452,13 +1347,29 @@ function watchScript(params) {
|
|
|
452
1347
|
provider: params.id,
|
|
453
1348
|
};
|
|
454
1349
|
}
|
|
455
|
-
const exit = await childClosed;
|
|
456
1350
|
if (outputLimitError) {
|
|
1351
|
+
await stopChild();
|
|
1352
|
+
await waitForChildClose(childClosed);
|
|
457
1353
|
throw outputLimitError;
|
|
458
1354
|
}
|
|
459
1355
|
if (childError) {
|
|
1356
|
+
await waitForChildClose(childClosed);
|
|
460
1357
|
throw new CrablineError(formatScriptError("Script watch command failed to start.", ensureErrorMessage(childError), params.command, diagnostics), { kind: "connectivity" });
|
|
461
1358
|
}
|
|
1359
|
+
const exit = await waitForChildCloseOrAbort(childClosed, [
|
|
1360
|
+
params.cancelSignal,
|
|
1361
|
+
params.context.signal,
|
|
1362
|
+
]);
|
|
1363
|
+
if (!exit) {
|
|
1364
|
+
await stopChild();
|
|
1365
|
+
if (params.cancelSignal.aborted) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (params.context.signal?.aborted) {
|
|
1369
|
+
throw params.context.signal.reason ?? new Error("Script watch command aborted.");
|
|
1370
|
+
}
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
462
1373
|
if (exit.code !== 0) {
|
|
463
1374
|
throw new CrablineError(formatScriptError(`Script watch command failed${exit.signal ? ` (${exit.signal})` : ""}.`, stderr, params.command, diagnostics), { kind: "connectivity" });
|
|
464
1375
|
}
|
|
@@ -476,13 +1387,13 @@ function watchScript(params) {
|
|
|
476
1387
|
throw error;
|
|
477
1388
|
}
|
|
478
1389
|
finally {
|
|
479
|
-
params.cancelSignal.removeEventListener("abort",
|
|
480
|
-
params.context.signal?.removeEventListener("abort",
|
|
1390
|
+
params.cancelSignal.removeEventListener("abort", requestStopChild);
|
|
1391
|
+
params.context.signal?.removeEventListener("abort", requestStopChild);
|
|
481
1392
|
child.stdin.destroy();
|
|
482
1393
|
if (!childCloseObserved) {
|
|
483
|
-
await
|
|
1394
|
+
await stopChild();
|
|
484
1395
|
}
|
|
485
|
-
await childClosed;
|
|
1396
|
+
await waitForChildClose(childClosed);
|
|
486
1397
|
}
|
|
487
1398
|
})();
|
|
488
1399
|
}
|