arcane-os 0.81.0 → 0.82.0

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.
@@ -0,0 +1,23 @@
1
+ import {connectCoreSocket, createCoreSocketConnection} from './socket-connection.mjs';
2
+
3
+ function reportAppControlError(error) { console.error('Arcane app control failed:', error); }
4
+
5
+ /** Connect to one explicitly selected running native window without launching it. */
6
+ export async function connectAppControl({endpoint, signal, onError = reportAppControlError} = {}) {
7
+ if (typeof endpoint !== 'string' || !endpoint) {
8
+ throw new TypeError('App control requires an explicit local pipe/socket endpoint.');
9
+ }
10
+ const socket = await connectCoreSocket(endpoint, signal, onError);
11
+ const connection = createCoreSocketConnection({socket, endpoint, signal, onError,
12
+ name: 'app-control', closedMessage: 'The app-control connection closed.'});
13
+ const {client} = connection;
14
+ return {
15
+ endpoint,
16
+ closed: connection.closed,
17
+ status(options) { return client.invoke('app.control.status', {}, options); },
18
+ inspect(parameters = {}, options) { return client.invoke('app.control.inspect', parameters, options); },
19
+ capture(parameters = {}, options) { return client.invoke('app.control.capture', parameters, options); },
20
+ act(parameters, options) { return client.invoke('app.control.act', parameters, options); },
21
+ close() { return connection.close(); }
22
+ };
23
+ }
@@ -0,0 +1,525 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.ComponentModel;
4
+ using System.IO;
5
+ using System.IO.Pipes;
6
+ using System.Runtime.InteropServices;
7
+ using System.Threading;
8
+ using System.Threading.Tasks;
9
+ using Microsoft.Win32.SafeHandles;
10
+
11
+ namespace Arcane.Core.Hosts.Windows
12
+ {
13
+ /// <summary>Controls one existing app window through its explicit pipe endpoint.</summary>
14
+ internal sealed class ArcaneAppControl
15
+ {
16
+ private readonly string endpoint;
17
+ private readonly Func<string, Dictionary<string, object>, CancellationToken, Task<object>> dispatch;
18
+ private readonly Action<string> onDiagnostic;
19
+ private readonly Action<Exception> onError;
20
+ private readonly object stateLock = new object();
21
+ private readonly HashSet<Connection> connections = new HashSet<Connection>();
22
+ private readonly List<Exception> failures = new List<Exception>();
23
+ private readonly CancellationTokenSource accepting = new CancellationTokenSource();
24
+ private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
25
+ private NamedPipeServerStream listener;
26
+ private Task lifetime;
27
+ private bool closing;
28
+
29
+ internal ArcaneAppControl(string endpoint,
30
+ Func<string, Dictionary<string, object>, CancellationToken, Task<object>> dispatch,
31
+ Action<string> onDiagnostic, Action<Exception> onError)
32
+ {
33
+ if (String.IsNullOrEmpty(endpoint)) throw new ArgumentException("Select the full app-control pipe endpoint.", "endpoint");
34
+ if (dispatch == null) throw new ArgumentNullException("dispatch");
35
+ if (onDiagnostic == null) throw new ArgumentNullException("onDiagnostic");
36
+ if (onError == null) throw new ArgumentNullException("onError");
37
+ this.endpoint = endpoint;
38
+ this.dispatch = dispatch;
39
+ this.onDiagnostic = onDiagnostic;
40
+ this.onError = onError;
41
+ // Claim before returning. An occupied endpoint never joins another
42
+ // app instance's pipe group or connects to that app as a client.
43
+ listener = CreateListener(true);
44
+ completion.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
45
+ lifetime = ListenAsync();
46
+ lifetime.ContinueWith(ListenerFailed, TaskContinuationOptions.OnlyOnFaulted);
47
+ }
48
+
49
+ internal Task Completion { get { return completion.Task; } }
50
+
51
+ internal Task CloseAsync()
52
+ {
53
+ StopIngress();
54
+ return Completion;
55
+ }
56
+
57
+ private NamedPipeServerStream CreateListener(bool first)
58
+ {
59
+ const uint duplex = 0x00000003;
60
+ const uint overlapped = 0x40000000;
61
+ const uint firstInstance = 0x00080000;
62
+ // Zero selects the platform's stream/wait modes and buffer defaults.
63
+ SafePipeHandle handle = CreateNamedPipe(endpoint,
64
+ duplex | overlapped | (first ? firstInstance : 0), 0, 255, 0, 0, 0, IntPtr.Zero);
65
+ if (handle.IsInvalid)
66
+ {
67
+ int code = Marshal.GetLastWin32Error();
68
+ handle.Dispose();
69
+ IOException error = new IOException("The app-control pipe endpoint could not be opened.", new Win32Exception(code));
70
+ error.Data["endpoint"] = endpoint;
71
+ throw error;
72
+ }
73
+ try { return new NamedPipeServerStream(PipeDirection.InOut, true, false, handle); }
74
+ catch { handle.Dispose(); throw; }
75
+ }
76
+
77
+ [DllImport("kernel32.dll", EntryPoint = "CreateNamedPipeW", CharSet = CharSet.Unicode, SetLastError = true)]
78
+ private static extern SafePipeHandle CreateNamedPipe(string name, uint openMode, uint pipeMode,
79
+ uint maximumInstances, uint outputBuffer, uint inputBuffer, uint defaultTimeout, IntPtr attributes);
80
+
81
+ private async Task ListenAsync()
82
+ {
83
+ try
84
+ {
85
+ while (true)
86
+ {
87
+ NamedPipeServerStream current;
88
+ lock (stateLock)
89
+ {
90
+ if (closing) break;
91
+ current = listener;
92
+ }
93
+ await current.WaitForConnectionAsync(accepting.Token).ConfigureAwait(false);
94
+ Connection connection;
95
+ lock (stateLock)
96
+ {
97
+ if (closing) break;
98
+ // Retain an instance continuously, including between
99
+ // accepts, so another host cannot claim this endpoint.
100
+ listener = CreateListener(false);
101
+ connection = new Connection(this, current);
102
+ connections.Add(connection);
103
+ }
104
+ connection.Start();
105
+ }
106
+ }
107
+ catch (OperationCanceledException error)
108
+ {
109
+ if (!accepting.IsCancellationRequested) Report(error);
110
+ }
111
+ catch (Exception error) { Report(error); }
112
+ StopIngress();
113
+ Connection[] active;
114
+ lock (stateLock) active = SnapshotConnections();
115
+ List<Task> drains = new List<Task>();
116
+ foreach (Connection connection in active) drains.Add(connection.Completion);
117
+ Task draining = Task.WhenAll(drains);
118
+ try { await draining.ConfigureAwait(false); }
119
+ catch (Exception error) { ReportTaskFailure(draining, error); }
120
+ // The final pipe handle owns the endpoint until accepted work
121
+ // settles, even though cancellation has stopped further accepts.
122
+ try { listener.Dispose(); }
123
+ catch (Exception error) { Report(error); }
124
+ accepting.Dispose();
125
+ Exception[] errors;
126
+ lock (stateLock) errors = failures.ToArray();
127
+ if (errors.Length == 0) completion.TrySetResult(null);
128
+ else completion.TrySetException(new AggregateException("App-control transport closed with errors.", errors));
129
+ }
130
+
131
+ private Connection[] SnapshotConnections()
132
+ {
133
+ Connection[] active = new Connection[connections.Count];
134
+ connections.CopyTo(active);
135
+ return active;
136
+ }
137
+
138
+ private void StopIngress()
139
+ {
140
+ Connection[] active;
141
+ lock (stateLock)
142
+ {
143
+ if (closing) return;
144
+ closing = true;
145
+ // The listener stays open as the endpoint claim. Cancellation
146
+ // stops its overlapped accept without releasing that ownership.
147
+ try { accepting.Cancel(); }
148
+ catch (Exception error) { Report(error); }
149
+ active = SnapshotConnections();
150
+ }
151
+ foreach (Connection connection in active) connection.Stop();
152
+ }
153
+
154
+ private void Remove(Connection connection)
155
+ {
156
+ lock (stateLock) connections.Remove(connection);
157
+ }
158
+
159
+ private void Report(Exception error)
160
+ {
161
+ lock (stateLock)
162
+ {
163
+ if (failures.Contains(error)) return;
164
+ failures.Add(error);
165
+ }
166
+ try { onError(error); }
167
+ catch (Exception callbackError) { lock (stateLock) failures.Add(callbackError); }
168
+ }
169
+
170
+ private void Diagnose(object record)
171
+ {
172
+ string diagnostic = null;
173
+ try
174
+ {
175
+ diagnostic = ArcaneHost.Serializer().Serialize(record) + Environment.NewLine;
176
+ onDiagnostic(diagnostic);
177
+ }
178
+ catch (Exception error)
179
+ {
180
+ IOException failure = new IOException("The app-control diagnostic could not be delivered.", error);
181
+ failure.Data["diagnosticRecord"] = record;
182
+ if (diagnostic != null) failure.Data["diagnostic"] = diagnostic;
183
+ Report(failure);
184
+ }
185
+ }
186
+
187
+ private void ReportTaskFailure(Task task, Exception caught)
188
+ {
189
+ if (task.Exception == null) { Report(caught); return; }
190
+ foreach (Exception error in task.Exception.InnerExceptions) Report(error);
191
+ }
192
+
193
+ private void ListenerFailed(Task task)
194
+ {
195
+ ReportTaskFailure(task, task.Exception);
196
+ completion.TrySetException(task.Exception);
197
+ }
198
+
199
+ private static void ObserveReportedFailure(Task task) { task.Exception.Handle(IgnoreReportedFailure); }
200
+ private static bool IgnoreReportedFailure(Exception error) { return true; }
201
+
202
+ private sealed class Connection
203
+ {
204
+ private readonly ArcaneAppControl owner;
205
+ private readonly NamedPipeServerStream pipe;
206
+ private readonly object stateLock = new object();
207
+ private readonly Dictionary<string, Request> requests = new Dictionary<string, Request>();
208
+ private readonly SemaphoreSlim writing = new SemaphoreSlim(1, 1);
209
+ private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
210
+ private Task lifetime;
211
+ private bool closed;
212
+
213
+ internal Connection(ArcaneAppControl owner, NamedPipeServerStream pipe)
214
+ {
215
+ this.owner = owner;
216
+ this.pipe = pipe;
217
+ }
218
+
219
+ internal Task Completion { get { return completion.Task; } }
220
+
221
+ internal void Start()
222
+ {
223
+ lifetime = ReadAsync();
224
+ lifetime.ContinueWith(Failed, TaskContinuationOptions.OnlyOnFaulted);
225
+ }
226
+
227
+ private async Task ReadAsync()
228
+ {
229
+ try
230
+ {
231
+ while (true)
232
+ {
233
+ lock (stateLock) { if (closed) break; }
234
+ string json = await ArcaneFrameTransport.ReadAsync(pipe).ConfigureAwait(false);
235
+ if (json == null) break;
236
+ Receive(json);
237
+ }
238
+ }
239
+ catch (ObjectDisposedException error) { ReadStopped(error); }
240
+ catch (OperationCanceledException error) { ReadStopped(error); }
241
+ catch (IOException error)
242
+ {
243
+ if (IsClosed()) ReadStopped(error);
244
+ else owner.Report(error);
245
+ }
246
+ catch (Exception error) { owner.Report(error); }
247
+ Stop();
248
+ List<Task> drains = new List<Task>();
249
+ lock (stateLock)
250
+ {
251
+ foreach (Request request in requests.Values) drains.Add(request.Completion);
252
+ }
253
+ Task draining = Task.WhenAll(drains);
254
+ try { await draining.ConfigureAwait(false); }
255
+ catch (Exception error) { owner.ReportTaskFailure(draining, error); }
256
+ writing.Dispose();
257
+ owner.Remove(this);
258
+ completion.TrySetResult(null);
259
+ }
260
+
261
+ private bool IsClosed() { lock (stateLock) return closed; }
262
+
263
+ private void ReadStopped(Exception error)
264
+ {
265
+ if (IsClosed()) owner.Diagnose(ArcaneHost.ErrorRecord(error, "ARCANE_APP_CONTROL_INPUT_CLOSED"));
266
+ else owner.Report(error);
267
+ }
268
+
269
+ private void Receive(string json)
270
+ {
271
+ try
272
+ {
273
+ Dictionary<string, object> frame = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
274
+ if (frame == null || Text(frame, "protocol") != "arcane/1")
275
+ throw new FormatException("Unknown app-control protocol.");
276
+ if (Text(frame, "type") == "control")
277
+ {
278
+ string control = Text(frame, "control");
279
+ if (control == "requests.cancelAll") { CancelRequests(); return; }
280
+ if (control == "request.cancel")
281
+ {
282
+ string id = Text(frame, "requestId");
283
+ if (String.IsNullOrEmpty(id)) throw new FormatException("Request cancellation requires a requestId.");
284
+ Request request;
285
+ lock (stateLock) requests.TryGetValue(id, out request);
286
+ if (request != null) request.Cancel();
287
+ return;
288
+ }
289
+ throw new FormatException("Unknown app-control cancellation control.");
290
+ }
291
+ string requestId = Text(frame, "id");
292
+ string method = Text(frame, "method");
293
+ if (Text(frame, "type") != "request" || String.IsNullOrEmpty(requestId) || method == null)
294
+ throw new FormatException("An app-control request requires an id and method.");
295
+ object parameters;
296
+ frame.TryGetValue("parameters", out parameters);
297
+ Request accepted;
298
+ lock (stateLock)
299
+ {
300
+ if (closed)
301
+ {
302
+ owner.Diagnose(new Dictionary<string, object>
303
+ {
304
+ { "appControlFrame", json }, { "reason", "connection-closed-before-dispatch" }
305
+ });
306
+ return;
307
+ }
308
+ if (requests.ContainsKey(requestId))
309
+ throw new FormatException("The app-control request id is already active on this connection.");
310
+ accepted = new Request(this, requestId, method, parameters, json);
311
+ requests.Add(requestId, accepted);
312
+ }
313
+ accepted.Start();
314
+ }
315
+ catch (Exception error)
316
+ {
317
+ error.Data["appControlFrame"] = json;
318
+ throw;
319
+ }
320
+ }
321
+
322
+ private static string Text(Dictionary<string, object> frame, string key)
323
+ {
324
+ object value;
325
+ return frame.TryGetValue(key, out value) ? value as string : null;
326
+ }
327
+
328
+ private void CancelRequests()
329
+ {
330
+ Request[] active;
331
+ lock (stateLock)
332
+ {
333
+ active = new Request[requests.Count];
334
+ requests.Values.CopyTo(active, 0);
335
+ }
336
+ foreach (Request request in active) request.Cancel();
337
+ }
338
+
339
+ internal void Stop()
340
+ {
341
+ lock (stateLock)
342
+ {
343
+ if (closed) return;
344
+ closed = true;
345
+ }
346
+ CancelRequests();
347
+ try { pipe.Dispose(); }
348
+ catch (Exception error) { owner.Report(error); }
349
+ }
350
+
351
+ private async Task<bool> SendAsync(Dictionary<string, object> response)
352
+ {
353
+ string json = ArcaneHost.Serializer().Serialize(response);
354
+ await writing.WaitAsync().ConfigureAwait(false);
355
+ try
356
+ {
357
+ if (IsClosed()) { RetireResponse(json, null); return true; }
358
+ await ArcaneFrameTransport.WriteAsync(pipe, json).ConfigureAwait(false);
359
+ return false;
360
+ }
361
+ catch (Exception error)
362
+ {
363
+ if (PeerDisconnected(error)
364
+ || (IsClosed() && (error is IOException || error is ObjectDisposedException || error is OperationCanceledException)))
365
+ {
366
+ Stop();
367
+ RetireResponse(json, error);
368
+ return true;
369
+ }
370
+ error.Data["appControlResponse"] = json;
371
+ throw;
372
+ }
373
+ finally { writing.Release(); }
374
+ }
375
+
376
+ private static bool PeerDisconnected(Exception error)
377
+ {
378
+ if (!(error is IOException)) return false;
379
+ // These platform results mean the peer ended the pipe, even
380
+ // when the write observes that before the reader sees EOF.
381
+ int code = error.HResult & 0xffff;
382
+ return code == 109 || code == 232 || code == 233;
383
+ }
384
+
385
+ private void RetireResponse(string json, Exception error)
386
+ {
387
+ Dictionary<string, object> record = new Dictionary<string, object>
388
+ {
389
+ { "appControlResponse", json }, { "reason", "connection-closed" }
390
+ };
391
+ if (error != null) record["error"] = ArcaneHost.ErrorRecord(error, "ARCANE_APP_CONTROL_OUTPUT_CLOSED");
392
+ owner.Diagnose(record);
393
+ }
394
+
395
+ private void Failed(Task task)
396
+ {
397
+ owner.ReportTaskFailure(task, task.Exception);
398
+ Stop();
399
+ owner.Remove(this);
400
+ completion.TrySetException(task.Exception);
401
+ }
402
+
403
+ private sealed class Request
404
+ {
405
+ private readonly Connection connection;
406
+ private readonly string id;
407
+ private readonly string method;
408
+ private readonly object parameters;
409
+ private readonly string frame;
410
+ private readonly object stateLock = new object();
411
+ private readonly CancellationTokenSource cancellation = new CancellationTokenSource();
412
+ private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
413
+ private Task lifetime;
414
+ private bool settled;
415
+
416
+ internal Request(Connection connection, string id, string method, object parameters, string frame)
417
+ {
418
+ this.connection = connection;
419
+ this.id = id;
420
+ this.method = method;
421
+ this.parameters = parameters;
422
+ this.frame = frame;
423
+ }
424
+
425
+ internal Task Completion { get { return completion.Task; } }
426
+
427
+ internal void Start()
428
+ {
429
+ lifetime = AnswerAsync();
430
+ lifetime.ContinueWith(Failed, TaskContinuationOptions.OnlyOnFaulted);
431
+ }
432
+
433
+ internal void Cancel()
434
+ {
435
+ lock (stateLock)
436
+ {
437
+ if (settled || cancellation.IsCancellationRequested) return;
438
+ try { cancellation.Cancel(); }
439
+ catch (Exception error) { connection.owner.Report(error); }
440
+ }
441
+ }
442
+
443
+ private async Task AnswerAsync()
444
+ {
445
+ Dictionary<string, object> response = new Dictionary<string, object>
446
+ {
447
+ { "protocol", "arcane/1" }, { "type", "response" }, { "id", id }
448
+ };
449
+ bool responseRecorded = false;
450
+ try
451
+ {
452
+ try
453
+ {
454
+ cancellation.Token.ThrowIfCancellationRequested();
455
+ if (method != "app.control.status" && method != "app.control.inspect"
456
+ && method != "app.control.capture" && method != "app.control.act")
457
+ throw new NotSupportedException("The app does not expose " + method + ".");
458
+ Dictionary<string, object> arguments = parameters as Dictionary<string, object>;
459
+ if (arguments == null)
460
+ throw new ArgumentException("App-control parameters must be an object.", "parameters");
461
+ // Dispatch owns the actual UI operation. Cancellation
462
+ // after it starts never fabricates a reversed action.
463
+ object result = await connection.owner.dispatch(method, arguments, cancellation.Token).ConfigureAwait(false);
464
+ response["ok"] = true;
465
+ response["result"] = result;
466
+ }
467
+ catch (Exception error)
468
+ {
469
+ error.Data["appControlFrame"] = frame;
470
+ response["ok"] = false;
471
+ string code = error is OperationCanceledException ? "REQUEST_ABORTED"
472
+ : error is NotSupportedException ? "METHOD_NOT_ALLOWED"
473
+ : error is ArgumentException ? "INVALID_ARGUMENT" : "ARCANE_APP_CONTROL_FAILED";
474
+ response["error"] = ArcaneHost.ErrorRecord(error, code);
475
+ }
476
+ response["time"] = DateTime.UtcNow.ToString("o");
477
+ responseRecorded = await connection.SendAsync(response).ConfigureAwait(false);
478
+ }
479
+ catch (Exception error)
480
+ {
481
+ if (!error.Data.Contains("appControlResponse")) error.Data["appControlResponse"] = response;
482
+ connection.owner.Report(error);
483
+ connection.Stop();
484
+ }
485
+ finally { Complete(response, responseRecorded); }
486
+ }
487
+
488
+ private void Complete(Dictionary<string, object> response, bool responseRecorded)
489
+ {
490
+ bool cancelled;
491
+ lock (stateLock)
492
+ {
493
+ if (settled) return;
494
+ cancelled = cancellation.IsCancellationRequested;
495
+ settled = true;
496
+ }
497
+ try
498
+ {
499
+ try { cancellation.Dispose(); }
500
+ catch (Exception error) { connection.owner.Report(error); }
501
+ // A live peer retires its cancelled request before this
502
+ // actual operation finishes, even while the pipe stays open.
503
+ if (cancelled && response != null && !responseRecorded)
504
+ connection.owner.Diagnose(new Dictionary<string, object>
505
+ {
506
+ { "appControlResponse", response }, { "reason", "request-cancelled" }
507
+ });
508
+ }
509
+ catch (Exception error) { connection.owner.Report(error); }
510
+ finally
511
+ {
512
+ try { lock (connection.stateLock) connection.requests.Remove(id); }
513
+ finally { completion.TrySetResult(null); }
514
+ }
515
+ }
516
+
517
+ private void Failed(Task task)
518
+ {
519
+ connection.owner.ReportTaskFailure(task, task.Exception);
520
+ Complete(null, false);
521
+ }
522
+ }
523
+ }
524
+ }
525
+ }
@@ -18,8 +18,6 @@ namespace Arcane.Core.Hosts.Windows
18
18
  public sealed class ArcaneCoreProcess
19
19
  {
20
20
  private static readonly Encoding Utf8 = new UTF8Encoding(false, true);
21
- private static readonly Encoding Ascii = Encoding.GetEncoding(
22
- "us-ascii", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
23
21
  private readonly object stateLock = new object();
24
22
  private readonly Process process;
25
23
  private readonly Action<string> onMessage;
@@ -142,13 +140,8 @@ namespace Arcane.Core.Hosts.Windows
142
140
  try
143
141
  {
144
142
  if (Volatile.Read(ref exitObserved) != 0) throw new IOException("Core exited before the accepted write completed.");
145
- byte[] body = Utf8.GetBytes(json);
146
- // Length exists only in the Content-Length transport frame.
147
- byte[] header = Ascii.GetBytes("Content-Length: " + body.Length.ToString(CultureInfo.InvariantCulture) + "\r\n\r\n");
148
143
  Stream input = process.StandardInput.BaseStream;
149
- await input.WriteAsync(header, 0, header.Length).ConfigureAwait(false);
150
- await input.WriteAsync(body, 0, body.Length).ConfigureAwait(false);
151
- await input.FlushAsync().ConfigureAwait(false);
144
+ await ArcaneFrameTransport.WriteAsync(input, json).ConfigureAwait(false);
152
145
  }
153
146
  catch (Exception error)
154
147
  {
@@ -214,7 +207,7 @@ namespace Arcane.Core.Hosts.Windows
214
207
  output = process.StandardOutput.BaseStream;
215
208
  while (true)
216
209
  {
217
- string message = await ReadFrameAsync(output).ConfigureAwait(false);
210
+ string message = await ArcaneFrameTransport.ReadAsync(output).ConfigureAwait(false);
218
211
  if (message == null) break;
219
212
  try { onMessage(message); }
220
213
  catch (Exception error)
@@ -306,60 +299,6 @@ namespace Arcane.Core.Hosts.Windows
306
299
  catch (Exception error) { Report(error); }
307
300
  }
308
301
 
309
- private static async Task<string> ReadFrameAsync(Stream stream)
310
- {
311
- using (MemoryStream header = new MemoryStream())
312
- using (MemoryStream body = new MemoryStream())
313
- {
314
- try
315
- {
316
- byte[] next = new byte[1];
317
- byte[] separator = new byte[] { 13, 10, 13, 10 };
318
- int matched = 0;
319
- while (matched != separator.Length)
320
- {
321
- int read = await stream.ReadAsync(next, 0, 1).ConfigureAwait(false);
322
- if (read == 0)
323
- {
324
- if (header.Length == 0) return null;
325
- throw new EndOfStreamException("Core stdout ended during a frame header.");
326
- }
327
- header.WriteByte(next[0]);
328
- matched = next[0] == separator[matched] ? matched + 1 : next[0] == 13 ? 1 : 0;
329
- }
330
- byte[] headerBytes = header.ToArray();
331
- string headerText = Ascii.GetString(headerBytes, 0, headerBytes.Length - separator.Length);
332
- long? contentLength = null;
333
- foreach (string line in headerText.Split(new string[] { "\r\n" }, StringSplitOptions.None))
334
- {
335
- if (!line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) continue;
336
- long length;
337
- if (contentLength.HasValue || !Int64.TryParse(line.Substring(15).Trim(), NumberStyles.None,
338
- CultureInfo.InvariantCulture, out length))
339
- throw new InvalidDataException("Core frame Content-Length is invalid.");
340
- contentLength = length;
341
- }
342
- if (!contentLength.HasValue) throw new InvalidDataException("Core frame Content-Length is missing.");
343
- long remaining = contentLength.Value;
344
- byte[] buffer = new byte[8192];
345
- while (remaining != 0)
346
- {
347
- int read = await stream.ReadAsync(buffer, 0, (int)Math.Min(remaining, buffer.Length)).ConfigureAwait(false);
348
- if (read == 0) throw new EndOfStreamException("Core stdout ended during a frame body.");
349
- body.Write(buffer, 0, read);
350
- remaining -= read;
351
- }
352
- return Utf8.GetString(body.ToArray());
353
- }
354
- catch (Exception error)
355
- {
356
- error.Data["frameHeader"] = header.ToArray();
357
- error.Data["frameBody"] = body.ToArray();
358
- throw;
359
- }
360
- }
361
- }
362
-
363
302
  private async Task CompleteAsync()
364
303
  {
365
304
  try