arcane-os 0.80.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.
- package/CHANGELOG.md +44 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
- package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
- package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
- package/browser-runtime/core/app-control.mjs +439 -0
- package/docs/reference/ai/browser-speech.md +11 -0
- package/docs/reference/ai/browser-wasm.md +21 -10
- package/docs/reference/cli.md +9 -1
- package/docs/reference/core-native-packaging.md +14 -0
- package/docs/reference/inventory/package-api.json +23 -9
- package/docs/reference/inventory/runtime-components.json +2 -1
- package/docs/reference/native-app-control.md +147 -0
- package/docs/reference/native-decisions.md +74 -16
- package/docs/reference/runtime-components.md +20 -0
- package/docs/reference/sdk-api.md +49 -6
- package/package.json +2 -1
- package/runtime/arcane/components/chat.html +140 -18
- package/runtime/arcane/modules/AI.js +10 -5
- package/src/cli/app-control.mjs +106 -0
- package/src/cli/main.mjs +5 -0
- package/src/core/app-control.mjs +23 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
- package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
- package/src/core/hosts/windows/ArcaneHost.cs +45 -7
- package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
- package/src/core/services/decisions.mjs +16 -2
- package/src/core/shared-host.mjs +7 -91
- package/src/core/socket-connection.mjs +109 -0
- package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
- package/src/local-ai/decision-tokenizer.mjs +2 -2
- package/src/local-ai/decisions.mjs +33 -10
|
@@ -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
|
|
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
|
|
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
|