dsh-pwsh-plugin 0.2.17 → 0.2.19

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.
@@ -1,225 +1,189 @@
1
- # pwsh_run 常驻会话服务端 —— 已实测验证的参考实现
2
- #
3
- # 启动:pwsh -NoProfile -NonInteractive -EncodedCommand <base64(utf16le, 本文件)>
4
- # - -EncodedCommand 而非 -File,是为了不落地临时脚本文件;
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
- # 原始 stdout 同样收回,避免用户脚本 [Console]::Out 直写污染协议行
49
- $SafeOut = [IO.StreamWriter]::new([IO.MemoryStream]::new(), $Utf8)
50
- [Console]::SetOut($SafeOut)
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
- # 孙子进程的 std handle 继承自 Win32 槽位,Console::SetIn/SetOut 只改 .NET 层,槽位里仍是
54
- # 真正的协议管道:脚本里一句 Start-Process 起的子进程就能往协议流里写(实测伪造一行
55
- # <<<BYE>>> 会让客户端当场判定会话死亡),也能读走客户端发来的调用数据。
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
- public int nLength;
79
- public IntPtr lpSecurityDescriptor;
80
- public bool bInheritHandle;
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
- # -PassThru 给出的是类型数组,取 [0] 才是那个类;直接按名字拿更直白。
100
- $null = Add-Type -TypeDefinition $Sig -ErrorAction Stop
101
- $K = [PwshRunStdio]
102
- $SA = [PwshRunStdio+SecurityAttributes]::new()
103
- $SA.nLength = $K::SecurityAttributesSize
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
- $TakeoverNote = 'pwsh-run: could not detach the child std handles (' + $_.Exception.Message + '); a script that starts its own processes may write into the session protocol stream and break this session.'
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
- $Fmt = @'
128
- $ErrorActionPreference = 'Continue'
129
- $__pwshn = 'ran'
113
+ $RS.SessionStateProxy.SetVariable('ErrorActionPreference', 'Continue')
130
114
  try {
131
- # *>&1 把全部流按发生顺序汇入管道(只写 2>&1 会漏 warning / verbose),
132
- # 再按对象类型逐条打标:顺序保真,同时保留 stdout / error / warning 的分流。
133
- . { __PWSHRUN_BODY__
134
- } *>&1 | ForEach-Object {
135
- if ($_ -is [System.Management.Automation.ErrorRecord]) { 'E:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_)) }
136
- elseif ($_ -is [System.Management.Automation.WarningRecord]) { 'W:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_)) }
137
- elseif ($_ -is [System.Management.Automation.VerboseRecord]) { 'V:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_)) }
138
- else { 'O:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_)) }
139
- }
140
- $__pwshn = 'ok'
141
- } catch {
142
- $__pwshn = 'threw'
143
- # PowerShell 会把真实异常包进一层 RuntimeException,EndInvoke 还会再包一层;
144
- # 逐层剥到最内层,模型看到的才是原始错误文本。
145
- $__e = $_.Exception
146
- while ($null -ne $__e.InnerException) { $__e = $__e.InnerException }
147
- 'X:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$__e.Message))
148
- } finally {
149
- 'S:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($__pwshn))
150
- }
151
- '@
152
-
153
- while ($true) {
154
- $line = $ProtoIn.ReadLine()
155
- if ($null -eq $line -or $line -eq 'QUIT') { break }
156
- if ($line.Length -eq 0) { continue }
157
- $sp1 = $line.IndexOf(' '); $sp2 = $line.IndexOf(' ', $sp1+1)
158
- $cid = $line.Substring(0, $sp1)
159
- $to = [double]$line.Substring($sp1+1, $sp2-$sp1-1)
160
- $src = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($line.Substring($sp2+1)))
161
- $ProtoOut.WriteLine('<<<B:' + $cid + '>>>')
162
- # 子进程往 drain 文件里写的东西没人要,每次调用前截回零,免得常驻会话把临时盘越写越满。
163
- # 截断失败不理会:那只是空间回收,不影响本调用。
164
- if ($null -ne $K -and $Drain.Length -gt 0) {
165
- try { [void]$K::SetFilePointer($hOut, 0, [IntPtr]::Zero, 0); [void]$K::SetEndOfFile($hOut) } catch {}
166
- }
167
- # 接管失败的说明只随第一次调用交付一次:模型需要知道这一次的会话对子进程不可靠,
168
- # 但把它挂在每次调用上只会变成噪音。
169
- if ($TakeoverNote.Length -gt 0) {
170
- $ProtoOut.WriteLine('W:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($TakeoverNote)))
171
- $TakeoverNote = ''
172
- }
173
- $PS = [PowerShell]::Create()
174
- $PS.Runspace = $RS
175
- $out = [System.Management.Automation.PSDataCollection[psobject]]::new()
176
- $a = $null
177
- try {
178
- [void]$PS.AddScript($Fmt.Replace('__PWSHRUN_BODY__', $src))
179
- $a = $PS.BeginInvoke[psobject, psobject]($null, $out)
180
- } catch {
181
- # 语法错误在 AddScript 阶段就抛 ParseException,不会走到 $Fmt 的 finally,
182
- # 所以这里必须自己补哨兵,否则客户端分不清「语法错」和「协议崩」。
183
- $__e = $_.Exception
184
- while ($null -ne $__e.InnerException) { $__e = $__e.InnerException }
185
- $ProtoOut.WriteLine('X:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$__e.Message)))
186
- $ProtoOut.WriteLine('S:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('threw')))
187
- }
188
- # 管道内每条输出已经是 "标记:base64" 的合法协议行(见 $Fmt),
189
- # 这里必须原样写出;再包一层 base64 会变成 "O:<b64(O:xxx)>",客户端解不出来。
190
- $emitted = 0; $timedOut = $false; $sawSentinel = $false
191
- if ($null -ne $a) {
192
- $deadline = [DateTime]::UtcNow.AddSeconds($to)
193
- while (-not $a.IsCompleted) {
194
- while ($out.Count -gt $emitted) { $item = $out[$emitted]; $emitted++; if ($null -ne $item) { if ([string]$item -like 'S:*') { $sawSentinel = $true }; $ProtoOut.WriteLine([string]$item) } }
195
- if ([DateTime]::UtcNow -gt $deadline) { $timedOut = $true; break }
196
- [void]$a.AsyncWaitHandle.WaitOne(30)
197
- }
198
- if ($timedOut) { try { $PS.Stop() } catch {}; [void]$a.AsyncWaitHandle.WaitOne(1500) }
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
- while ($out.Count -gt $emitted) { $item = $out[$emitted]; $emitted++; if ($null -ne $item) { if ([string]$item -like 'S:*') { $sawSentinel = $true }; $ProtoOut.WriteLine([string]$item) } }
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
- $ProtoOut.WriteLine('<<<T:' + $cid + ':' + [int]$timedOut + '>>>')
222
- $ProtoOut.WriteLine('<<<E:' + $cid + '>>>')
223
- $ProtoOut.Flush()
224
- }
225
- $ProtoOut.WriteLine('<<<BYE>>>'); $ProtoOut.Flush()
188
+ } finally { $RS.Dispose() }
189
+ $ProtoOut.WriteLine('<<<BYE>>>')