dsh-pwsh-plugin 0.2.17 → 0.2.18
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 +46 -20
- package/README.zh-CN.md +39 -15
- package/docs/dsh-windows-schannel.zh-CN.md +118 -0
- package/docs/pwsh-run-audit.zh-CN.md +183 -0
- package/docs/runtime-notes.md +23 -19
- package/docs/runtime-notes.zh-CN.md +17 -15
- package/package.json +3 -3
- package/plugin/index.mjs +155 -193
- package/plugin/session-server.ps1 +166 -202
- package/plugin/session.mjs +160 -152
|
@@ -1,225 +1,189 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
# 启动字符串是 base64(UTF-16LE, 本文件);命令行上限 32K,本文件换算后约 16.6K,安全在内。
|
|
6
|
-
# - 不要用 -Command -:那是"读到 EOF 就退出"的一次性模式,无法常驻。
|
|
7
|
-
#
|
|
8
|
-
# 协议(客户端 → 服务端,每行一条):
|
|
9
|
-
# <id> <timeoutSeconds> <base64(utf8 脚本文本)>
|
|
10
|
-
# 保留控制行:QUIT
|
|
11
|
-
#
|
|
12
|
-
# 响应(服务端 → 客户端,每行一条):
|
|
13
|
-
# <<<B:id>>> 本次调用的输出开始
|
|
14
|
-
# O:<base64> 输出流对象
|
|
15
|
-
# E:<base64> ErrorRecord(Write-Error / 非终止错误)
|
|
16
|
-
# W:<base64> WarningRecord
|
|
17
|
-
# X:<base64> 异常消息(throw 或包装层自身异常)
|
|
18
|
-
# S:<base64> 哨兵:ok=正常跑完 / threw=抛异常 / ran=被 exit 或超时打断
|
|
19
|
-
# F:<base64> BeginInvoke 本身失败
|
|
20
|
-
# <<<T:id:0|1>>> 结束标记,1 表示超时被打断
|
|
21
|
-
# <<<E:id>>> 整条响应结束
|
|
22
|
-
# <<<BYE>>> 进程正常退出
|
|
23
|
-
#
|
|
24
|
-
# 关键设计决定(每条都有实测依据,见 ../../FINDINGS.md):
|
|
25
|
-
# 1. 每次调用新建 PowerShell 实例、共享同一 Runspace。
|
|
26
|
-
# 单例实例在 throw 之后会让下一次调用静默返回空输出。
|
|
27
|
-
# 2. 原始 stdin/stdout 被协议独占,Console.In/Out/Error 换成安全流。
|
|
28
|
-
# 否则用户脚本一句 [Console]::In.ReadLine() 就会吃掉协议数据、整个会话失联。
|
|
29
|
-
# 3. *>&1 而非 2>&1:只合并 error 会漏掉 warning / verbose。
|
|
30
|
-
# 4. 边跑边吐($out 增量读取):超时被打断前的部分输出也能交付。
|
|
31
|
-
# 5. 每次调用前重置 $ErrorActionPreference:脚本内设成 'Stop' 会跨调用污染。
|
|
32
|
-
|
|
33
|
-
$ErrorActionPreference = 'Continue'
|
|
1
|
+
# stdin: id timeoutMs base64(utf8 JSON {script,workdir}) outputByteLimit
|
|
2
|
+
# stdout: B/T/E framing; typed UTF-8 base64 records; '+' continues one record; L: reports truncation.
|
|
3
|
+
# Console and inherited child handles must never carry protocol traffic.
|
|
4
|
+
$ErrorActionPreference = 'Stop'
|
|
34
5
|
$Utf8 = [Text.UTF8Encoding]::new($false)
|
|
35
|
-
# 子进程的 stdout 是按它自己的编码吐的(node / git / gh 都是 UTF-8),PowerShell 用
|
|
36
|
-
# Console.OutputEncoding 去解码这些字节。这个值默认跟随系统 ANSI 代码页(本机是 gb2312),
|
|
37
|
-
# 于是「node -e "console.log('中文')"」在常驻会话里全变成乱码——原生 pwsh 工具没这个问题,
|
|
38
|
-
# 因为它的启动前导显式设了 UTF-8。这里补上同一句。
|
|
39
6
|
[Console]::OutputEncoding = $Utf8
|
|
40
7
|
$OutputEncoding = $Utf8
|
|
41
|
-
# 原始 stdin 交给协议读取器独占;再把 Console.In 换成安全空流
|
|
42
|
-
# 否则用户脚本一句 [Console]::In.ReadLine() 就会吃掉协议数据,整个会话失联(已实测)
|
|
43
8
|
$ProtoIn = [IO.StreamReader]::new([Console]::OpenStandardInput(), $Utf8)
|
|
44
|
-
$SafeIn = [IO.StreamReader]::new([IO.MemoryStream]::new([byte[]]@()), $Utf8)
|
|
45
|
-
[Console]::SetIn($SafeIn)
|
|
46
9
|
$ProtoOut = [IO.StreamWriter]::new([Console]::OpenStandardOutput(), $Utf8)
|
|
47
10
|
$ProtoOut.AutoFlush = $true
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
[Console]::
|
|
51
|
-
[Console]::SetError($SafeOut)
|
|
11
|
+
[Console]::SetIn([IO.StringReader]::new(''))
|
|
12
|
+
[Console]::SetOut([IO.TextWriter]::Null)
|
|
13
|
+
[Console]::SetError([IO.TextWriter]::Null)
|
|
52
14
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
# 所以拿到协议流之后要把三个槽位整体换掉:stdin 接 NUL(子进程读到 EOF),
|
|
57
|
-
# stdout/stderr 接一个带 FILE_FLAG_DELETE_ON_CLOSE 的临时文件(子进程写进去,随进程退出自动消失)。
|
|
58
|
-
# stdout 不能接 NUL:NUL 是字符设备,PowerShell 会认为输出没被重定向,于是 & exe 的报错从
|
|
59
|
-
# 「failed to run: 拒绝访问」变成「StandardOutputEncoding is only supported when standard output
|
|
60
|
-
# is redirected」,沙箱边界的诊断信息就丢了。磁盘句柄没有这个问题(实测两种文案只随句柄类型变)。
|
|
61
|
-
# 接管失败只降级不中断:它是加固,不是本工具能干活的前提;原因随第一次调用的警告行告知模型。
|
|
62
|
-
$TakeoverNote = ''
|
|
63
|
-
$K = $null
|
|
64
|
-
$Drain = ''
|
|
65
|
-
$hOut = [IntPtr]::Zero
|
|
66
|
-
try {
|
|
67
|
-
# 句柄必须带 bInheritHandle 才交得给子进程。用 NULL 安全属性建的句柄不可继承,子进程按
|
|
68
|
-
# STARTUPINFO.hStdInput 拿到的就只是"父进程的句柄号",在本进程里没有对应对象:
|
|
69
|
-
# GetFileType 报 UNKNOWN,偶尔这个编号还被别的对象复用,读出来是别人的字节(实测 20 次里有 1 次
|
|
70
|
-
# 读到一个 PE 头)。带上继承位之后 30 次全是字符设备、每次都读到 EOF。
|
|
71
|
-
$Sig = @'
|
|
15
|
+
# C# callbacks drain collections on the producer thread. PowerShell scriptblock event handlers
|
|
16
|
+
# need a runspace and cannot safely serve as callbacks from the pipeline's worker thread.
|
|
17
|
+
$Types = @'
|
|
72
18
|
using System;
|
|
19
|
+
using System.IO;
|
|
20
|
+
using System.Text;
|
|
21
|
+
using System.Management.Automation;
|
|
73
22
|
using System.Runtime.InteropServices;
|
|
74
23
|
|
|
24
|
+
public sealed class PwshRunOutput {
|
|
25
|
+
readonly TextWriter writer;
|
|
26
|
+
readonly object gate = new object();
|
|
27
|
+
int outputLeft, errorLeft = 65536;
|
|
28
|
+
int outputRecords, errorRecords;
|
|
29
|
+
bool truncated;
|
|
30
|
+
public PwshRunOutput(TextWriter writer, int limit) { this.writer = writer; outputLeft = limit; }
|
|
31
|
+
public void Emit(string kind, string text) {
|
|
32
|
+
lock (gate) {
|
|
33
|
+
text = text ?? "";
|
|
34
|
+
bool error = kind == "E" || kind == "X" || kind == "F";
|
|
35
|
+
int separator = (error ? errorRecords : outputRecords) > 0 ? 1 : 0;
|
|
36
|
+
int left = (error ? errorLeft : outputLeft) - separator;
|
|
37
|
+
if (left < 0) {
|
|
38
|
+
if (!truncated) { writer.WriteLine("L:"); truncated = true; }
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
int length = text.Length;
|
|
42
|
+
if (Encoding.UTF8.GetByteCount(text) > left) {
|
|
43
|
+
int lo = 0, hi = length;
|
|
44
|
+
while (lo < hi) {
|
|
45
|
+
int mid = lo + (hi - lo + 1) / 2;
|
|
46
|
+
if (Encoding.UTF8.GetByteCount(text.Substring(0, mid)) <= left) lo = mid;
|
|
47
|
+
else hi = mid - 1;
|
|
48
|
+
}
|
|
49
|
+
length = lo;
|
|
50
|
+
if (length > 0 && char.IsHighSurrogate(text[length - 1])) length--;
|
|
51
|
+
if (!truncated) { writer.WriteLine("L:"); truncated = true; }
|
|
52
|
+
}
|
|
53
|
+
int bytes = Encoding.UTF8.GetByteCount(text.Substring(0, length));
|
|
54
|
+
if (length == 0 && text.Length != 0) return;
|
|
55
|
+
if (error) { errorLeft -= bytes + separator; errorRecords++; }
|
|
56
|
+
else { outputLeft -= bytes + separator; outputRecords++; }
|
|
57
|
+
int offset = 0;
|
|
58
|
+
do {
|
|
59
|
+
int count = Math.Min(4096, length - offset);
|
|
60
|
+
if (count > 0 && offset + count < length && char.IsHighSurrogate(text[offset + count - 1])) count--;
|
|
61
|
+
string payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(text.Substring(offset, count)));
|
|
62
|
+
offset += count;
|
|
63
|
+
writer.WriteLine(kind + (offset < length ? "+:" : ":") + payload);
|
|
64
|
+
} while (offset < length);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
void Wire<T>(PSDataCollection<T> items, string kind) {
|
|
68
|
+
items.DataAdded += (sender, args) => {
|
|
69
|
+
foreach (T item in items.ReadAll()) Emit(kind, item == null ? "" : item.ToString());
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
public void Attach(PowerShell ps, PSDataCollection<PSObject> output) {
|
|
73
|
+
Wire(output, "O"); Wire(ps.Streams.Error, "E"); Wire(ps.Streams.Warning, "W");
|
|
74
|
+
Wire(ps.Streams.Verbose, "V"); Wire(ps.Streams.Debug, "D"); Wire(ps.Streams.Information, "I");
|
|
75
|
+
// Progress is not rendered by this noninteractive tool, but must not accumulate.
|
|
76
|
+
ps.Streams.Progress.DataAdded += (sender, args) => { ps.Streams.Progress.ReadAll(); };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
public static class PwshRunStdio {
|
|
76
81
|
[StructLayout(LayoutKind.Sequential)]
|
|
77
|
-
public struct SecurityAttributes {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
public struct SecurityAttributes { public int nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; }
|
|
83
|
+
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
84
|
+
static extern IntPtr CreateFileW(string n, uint a, uint s, ref SecurityAttributes sa, uint d, uint f, IntPtr t);
|
|
85
|
+
[DllImport("kernel32.dll", SetLastError=true)]
|
|
86
|
+
static extern bool SetStdHandle(int n, IntPtr h);
|
|
87
|
+
public static void Detach() {
|
|
88
|
+
var sa = new SecurityAttributes { nLength = Marshal.SizeOf(typeof(SecurityAttributes)), bInheritHandle = true };
|
|
89
|
+
var input = CreateFileW("NUL", 0x80000000, 3, ref sa, 3, 0, IntPtr.Zero);
|
|
90
|
+
var output = CreateFileW("NUL", 0x40000000, 3, ref sa, 3, 0, IntPtr.Zero);
|
|
91
|
+
if (input == IntPtr.Zero || input == new IntPtr(-1) || output == IntPtr.Zero || output == new IntPtr(-1)
|
|
92
|
+
|| !SetStdHandle(-10, input) || !SetStdHandle(-11, output) || !SetStdHandle(-12, output))
|
|
93
|
+
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Could not detach child std handles");
|
|
81
94
|
}
|
|
82
|
-
|
|
83
|
-
/// <summary>结构体字节数。放在 C# 里算:PowerShell 会把 Marshal.SizeOf(type) 绑到 object 重载上并报错。</summary>
|
|
84
|
-
public static int SecurityAttributesSize { get { return Marshal.SizeOf(typeof(SecurityAttributes)); } }
|
|
85
|
-
|
|
86
|
-
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
87
|
-
public static extern IntPtr CreateFileW(string n, uint a, uint s, ref SecurityAttributes sa, uint d, uint f, IntPtr t);
|
|
88
|
-
|
|
89
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
90
|
-
public static extern bool SetStdHandle(int n, IntPtr h);
|
|
91
|
-
|
|
92
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
93
|
-
public static extern uint SetFilePointer(IntPtr h, int lo, IntPtr hi, uint method);
|
|
94
|
-
|
|
95
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
96
|
-
public static extern bool SetEndOfFile(IntPtr h);
|
|
97
95
|
}
|
|
98
96
|
'@
|
|
99
|
-
|
|
100
|
-
$null = Add-Type -TypeDefinition $
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
$
|
|
104
|
-
$SA.bInheritHandle = $true
|
|
105
|
-
$Invalid = [IntPtr]::new(-1)
|
|
106
|
-
# 0x40000000 GENERIC_WRITE / 0x80000000 GENERIC_READ / 0x3 share read+write / 0x3 OPEN_EXISTING
|
|
107
|
-
$NulR = $K::CreateFileW('NUL', [uint32]2147483648, [uint32]3, [ref]$SA, [uint32]3, [uint32]0, [IntPtr]::Zero)
|
|
108
|
-
if ($NulR -eq [IntPtr]::Zero -or $NulR -eq $Invalid) { throw 'CreateFileW(NUL, read) failed' }
|
|
109
|
-
if (-not $K::SetStdHandle(-10, $NulR)) { throw 'SetStdHandle(stdin) failed' }
|
|
110
|
-
# 0x2 CREATE_ALWAYS / 0x7 share read+write+delete / 0x04000000 FILE_FLAG_DELETE_ON_CLOSE
|
|
111
|
-
$Drain = [IO.Path]::Combine([IO.Path]::GetTempPath(), 'pwsh-run-drain-' + [Guid]::NewGuid().ToString('N') + '.log')
|
|
112
|
-
$hOut = $K::CreateFileW($Drain, [uint32]1073741824, [uint32]7, [ref]$SA, [uint32]2, [uint32]67108864, [IntPtr]::Zero)
|
|
113
|
-
if ($hOut -eq [IntPtr]::Zero -or $hOut -eq $Invalid) {
|
|
114
|
-
# 临时目录不可写时退回 NUL:协议流仍然干净,只是 & exe 的报错文案会变成重定向那一句。
|
|
115
|
-
$Drain = ''
|
|
116
|
-
$hOut = $K::CreateFileW('NUL', [uint32]1073741824, [uint32]3, [ref]$SA, [uint32]3, [uint32]0, [IntPtr]::Zero)
|
|
117
|
-
if ($hOut -eq [IntPtr]::Zero -or $hOut -eq $Invalid) { throw 'CreateFileW(stdout) failed' }
|
|
118
|
-
}
|
|
119
|
-
if (-not $K::SetStdHandle(-11, $hOut)) { throw 'SetStdHandle(stdout) failed' }
|
|
120
|
-
if (-not $K::SetStdHandle(-12, $hOut)) { throw 'SetStdHandle(stderr) failed' }
|
|
97
|
+
try {
|
|
98
|
+
$null = Add-Type -TypeDefinition $Types -ErrorAction Stop
|
|
99
|
+
# NUL bounds discarded native output without any temporary drain file. Its handle type can
|
|
100
|
+
# change PowerShell's native-launch denial text; the client recognizes that diagnostic too.
|
|
101
|
+
if ($IsWindows) { [PwshRunStdio]::Detach() }
|
|
121
102
|
} catch {
|
|
122
|
-
|
|
103
|
+
# A partially detached process must not accept user scripts and expose the protocol handles.
|
|
104
|
+
$ProtoOut.WriteLine('<<<B:c1>>>')
|
|
105
|
+
$ProtoOut.WriteLine('F:' + [Convert]::ToBase64String($Utf8.GetBytes($_.Exception.Message)))
|
|
106
|
+
$ProtoOut.WriteLine('S:' + [Convert]::ToBase64String($Utf8.GetBytes('threw')))
|
|
107
|
+
$ProtoOut.WriteLine('<<<E:c1>>>')
|
|
108
|
+
exit 1
|
|
123
109
|
}
|
|
124
110
|
|
|
125
111
|
$RS = [RunspaceFactory]::CreateRunspace()
|
|
126
112
|
$RS.Open()
|
|
127
|
-
$
|
|
128
|
-
$ErrorActionPreference = 'Continue'
|
|
129
|
-
$__pwshn = 'ran'
|
|
113
|
+
$RS.SessionStateProxy.SetVariable('ErrorActionPreference', 'Continue')
|
|
130
114
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
else {
|
|
200
|
-
try { [void]$PS.EndInvoke($a) } catch {
|
|
201
|
-
# 语法错误不会进 $Fmt 的 catch(脚本根本没跑起来),而是从这里冒出,
|
|
202
|
-
# 同样要剥掉 EndInvoke 的包装层再交付。
|
|
203
|
-
$__e = $_.Exception
|
|
204
|
-
while ($null -ne $__e.InnerException) { $__e = $__e.InnerException }
|
|
205
|
-
$ProtoOut.WriteLine('X:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$__e.Message)))
|
|
115
|
+
while ($true) {
|
|
116
|
+
$line = $ProtoIn.ReadLine()
|
|
117
|
+
if ($null -eq $line -or $line -eq 'QUIT') { break }
|
|
118
|
+
if ($line.Length -eq 0) { continue }
|
|
119
|
+
$parts = $line.Split(' ', 4)
|
|
120
|
+
$cid = $parts[0]
|
|
121
|
+
$timeoutMs = [int]$parts[1]
|
|
122
|
+
$request = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[2])) | ConvertFrom-Json
|
|
123
|
+
$limit = [int]$parts[3]
|
|
124
|
+
$ProtoOut.WriteLine('<<<B:' + $cid + '>>>')
|
|
125
|
+
$sink = [PwshRunOutput]::new($ProtoOut, $limit)
|
|
126
|
+
$PS = [PowerShell]::Create()
|
|
127
|
+
$PS.Runspace = $RS
|
|
128
|
+
$out = [System.Management.Automation.PSDataCollection[psobject]]::new()
|
|
129
|
+
$sink.Attach($PS, $out)
|
|
130
|
+
$timedOut = $false
|
|
131
|
+
$sentinel = 'threw'
|
|
132
|
+
try {
|
|
133
|
+
if ($request.workdir) {
|
|
134
|
+
[void]$PS.AddCommand('Microsoft.PowerShell.Management\Set-Location').AddParameter('LiteralPath', $request.workdir).AddParameter('ErrorAction', 'Stop')
|
|
135
|
+
[void]$PS.Invoke()
|
|
136
|
+
if ($PS.HadErrors) { throw 'Could not set the requested working directory' }
|
|
137
|
+
$PS.Commands.Clear()
|
|
138
|
+
}
|
|
139
|
+
# AddScript does not enforce #requires; check its AST contract before user code runs.
|
|
140
|
+
$requirements = [scriptblock]::Create($request.script).Ast.ScriptRequirements
|
|
141
|
+
if ($requirements) {
|
|
142
|
+
if ($requirements.RequiredPSVersion -and $PSVersionTable.PSVersion -lt $requirements.RequiredPSVersion) {
|
|
143
|
+
throw ('Script requires PowerShell ' + $requirements.RequiredPSVersion)
|
|
144
|
+
}
|
|
145
|
+
if ($requirements.RequiredPSEditions.Count -gt 0 -and $requirements.RequiredPSEditions -notcontains $PSVersionTable.PSEdition) {
|
|
146
|
+
throw ('Script requires PowerShell edition ' + ($requirements.RequiredPSEditions -join ', '))
|
|
147
|
+
}
|
|
148
|
+
if ($requirements.IsElevationRequired) {
|
|
149
|
+
$elevated = if ($IsWindows) {
|
|
150
|
+
[Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
151
|
+
} else { [Environment]::UserName -eq 'root' }
|
|
152
|
+
if (-not $elevated) { throw 'Script requires administrator privileges' }
|
|
153
|
+
}
|
|
154
|
+
if ($requirements.RequiredApplicationId -or $requirements.RequiredAssemblies.Count -gt 0) {
|
|
155
|
+
throw 'pwsh_run does not support #requires -ShellId or -Assembly; load assemblies explicitly'
|
|
156
|
+
}
|
|
157
|
+
if ($requirements.RequiredModules.Count -gt 0) {
|
|
158
|
+
[void]$PS.AddCommand('Microsoft.PowerShell.Core\Import-Module').AddParameter('FullyQualifiedName', $requirements.RequiredModules).AddParameter('ErrorAction', 'Stop')
|
|
159
|
+
[void]$PS.Invoke()
|
|
160
|
+
if ($PS.HadErrors) { throw 'Could not load the modules required by the script' }
|
|
161
|
+
$PS.Commands.Clear()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
# Run the user's script at top level; formatting is the following pipeline command.
|
|
165
|
+
[void]$PS.AddScript($request.script, $false).AddCommand('Microsoft.PowerShell.Utility\Out-String').AddParameter('Stream').AddParameter('Width', 200)
|
|
166
|
+
$a = $PS.BeginInvoke[psobject, psobject]($null, $out)
|
|
167
|
+
if (-not $a.AsyncWaitHandle.WaitOne($timeoutMs)) {
|
|
168
|
+
$timedOut = $true
|
|
169
|
+
$PS.Stop()
|
|
170
|
+
}
|
|
171
|
+
[void]$PS.EndInvoke($a)
|
|
172
|
+
$sentinel = 'ok'
|
|
173
|
+
} catch {
|
|
174
|
+
if ($timedOut) { $sentinel = 'ran' }
|
|
175
|
+
else {
|
|
176
|
+
$cause = $_.Exception
|
|
177
|
+
while ($null -ne $cause.InnerException) { $cause = $cause.InnerException }
|
|
178
|
+
$sink.Emit('X', $cause.Message)
|
|
179
|
+
}
|
|
180
|
+
} finally {
|
|
181
|
+
$PS.Dispose()
|
|
182
|
+
$out.Dispose()
|
|
206
183
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# 错误 / 警告已在管道内按类型打标,这里不再重复汇总,
|
|
211
|
-
# 只兜底那些没能进入管道的残留(例如 runspace 级错误)。
|
|
212
|
-
foreach ($er in $PS.Streams.Error) { $ProtoOut.WriteLine('E:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$er))) }
|
|
213
|
-
$PS.Streams.Error.Clear()
|
|
214
|
-
$PS.Dispose()
|
|
215
|
-
# 哨兵兜底:$Fmt 没能执行(语法错误 / BeginInvoke 就失败)时补一条,
|
|
216
|
-
# 让客户端始终能区分「跑完了」「抛异常了」「被中断了」。
|
|
217
|
-
if (-not $sawSentinel) {
|
|
218
|
-
$__n = 'threw'; if ($timedOut) { $__n = 'ran' }
|
|
219
|
-
$ProtoOut.WriteLine('S:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($__n)))
|
|
184
|
+
$ProtoOut.WriteLine('S:' + [Convert]::ToBase64String($Utf8.GetBytes($sentinel)))
|
|
185
|
+
$ProtoOut.WriteLine('<<<T:' + $cid + ':' + [int]$timedOut + '>>>')
|
|
186
|
+
$ProtoOut.WriteLine('<<<E:' + $cid + '>>>')
|
|
220
187
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
$ProtoOut.Flush()
|
|
224
|
-
}
|
|
225
|
-
$ProtoOut.WriteLine('<<<BYE>>>'); $ProtoOut.Flush()
|
|
188
|
+
} finally { $RS.Dispose() }
|
|
189
|
+
$ProtoOut.WriteLine('<<<BYE>>>')
|