arcane-os 0.56.0 → 0.57.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/core-local.mjs +306 -0
  4. package/docs/reference/README.md +20 -0
  5. package/docs/reference/cli.md +9 -1
  6. package/docs/reference/core-native-packaging.md +195 -11
  7. package/docs/reference/inventory/package-api.json +138 -12
  8. package/docs/reference/local-ai.md +197 -0
  9. package/docs/reference/protocols.md +69 -1
  10. package/docs/reference/runtime-modules.md +6 -5
  11. package/docs/reference/sdk-api.md +165 -26
  12. package/package.json +5 -1
  13. package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
  14. package/schemas/arcane-app.schema.json +54 -0
  15. package/schemas/arcane-package.schema.json +44 -0
  16. package/src/app-descriptor.mjs +17 -1
  17. package/src/cli/main.mjs +8 -2
  18. package/src/core/host.mjs +23 -0
  19. package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
  20. package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
  21. package/src/core/hosts/windows/ArcaneCoreProcess.cs +3 -3
  22. package/src/core/hosts/windows/ArcaneHost.cs +695 -0
  23. package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
  24. package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
  25. package/src/core/services/local-ai.mjs +543 -0
  26. package/src/dev-server.mjs +7 -0
  27. package/src/import-map.mjs +1 -0
  28. package/src/local-ai/archive.mjs +52 -0
  29. package/src/local-ai/config.mjs +62 -0
  30. package/src/local-ai/dev.mjs +390 -0
  31. package/src/local-ai/discover.mjs +71 -0
  32. package/src/local-ai/http.mjs +106 -0
  33. package/src/local-ai/install.mjs +344 -0
  34. package/src/local-ai/server.mjs +262 -0
  35. package/src/native/portable-layout.mjs +29 -4
  36. package/src/native/portable-provider.mjs +39 -8
  37. package/src/native/windows-provider.mjs +262 -0
  38. package/src/native-provider-loader.mjs +8 -2
  39. package/src/packager/core.mjs +6 -2
  40. package/src/process.mjs +66 -8
  41. package/src/toolchain.mjs +33 -5
@@ -0,0 +1,695 @@
1
+ using System;
2
+ using System.Collections;
3
+ using System.Collections.Generic;
4
+ using System.IO;
5
+ using System.Runtime.InteropServices;
6
+ using System.Threading;
7
+ using System.Threading.Tasks;
8
+ using System.Web.Script.Serialization;
9
+ using System.Windows.Forms;
10
+ using Microsoft.Web.WebView2.Core;
11
+ using Microsoft.Web.WebView2.WinForms;
12
+
13
+ namespace Arcane.Core.Hosts.Windows
14
+ {
15
+ /// <summary>Selected by the application launcher; no OS layout is inferred.</summary>
16
+ public sealed class ArcaneHostOptions
17
+ {
18
+ public string ApplicationRoot { get; set; }
19
+ public string StartPath { get; set; }
20
+ public string OriginHost { get; set; }
21
+ public string ProfileDirectory { get; set; }
22
+ public string Title { get; set; }
23
+ public string ClassicClientSource { get; set; }
24
+ public string CoreExecutable { get; set; }
25
+ public string CoreArguments { get; set; }
26
+ public string CoreWorkingDirectory { get; set; }
27
+ }
28
+
29
+ public static class ArcaneHost
30
+ {
31
+ /// <summary>
32
+ /// Called by an STA launcher after it resolves its own launch context.
33
+ /// The message loop remains active while accepted Core work drains.
34
+ /// </summary>
35
+ public static void Run(ArcaneHostOptions options, Action<string> onDiagnostic,
36
+ Action<Exception> onError = null)
37
+ {
38
+ if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
39
+ throw new InvalidOperationException("The Windows WebView2 host requires an STA entry point.");
40
+ Application.EnableVisualStyles();
41
+ Application.SetCompatibleTextRenderingDefault(false);
42
+ using (ArcaneHostForm window = new ArcaneHostForm(options, onDiagnostic, onError))
43
+ {
44
+ Application.Run(window);
45
+ // FormClosing keeps the loop alive until this drain is complete.
46
+ window.Completion.GetAwaiter().GetResult();
47
+ }
48
+ }
49
+
50
+ internal static JavaScriptSerializer Serializer()
51
+ {
52
+ return new JavaScriptSerializer { MaxJsonLength = Int32.MaxValue, RecursionLimit = Int32.MaxValue };
53
+ }
54
+
55
+ internal static object ErrorRecord(Exception error, string code)
56
+ {
57
+ Dictionary<string, object> record = new Dictionary<string, object>
58
+ {
59
+ { "code", code }, { "name", error.GetType().FullName },
60
+ { "message", error.Message }, { "stack", error.StackTrace },
61
+ { "technicalMessage", error.ToString() }, { "hresult", error.HResult }
62
+ };
63
+ if (error.InnerException != null) record["cause"] = ErrorRecord(error.InnerException, code);
64
+ AggregateException aggregate = error as AggregateException;
65
+ if (aggregate != null)
66
+ {
67
+ List<object> errors = new List<object>();
68
+ foreach (Exception inner in aggregate.InnerExceptions) errors.Add(ErrorRecord(inner, code));
69
+ record["errors"] = errors;
70
+ }
71
+ if (error.Data.Count != 0)
72
+ {
73
+ List<object> entries = new List<object>();
74
+ foreach (DictionaryEntry entry in error.Data)
75
+ entries.Add(new Dictionary<string, object> { { "key", entry.Key }, { "value", entry.Value } });
76
+ record["data"] = entries;
77
+ }
78
+ return record;
79
+ }
80
+ }
81
+
82
+ /// <summary>
83
+ /// Owns one window and Core child. Call CloseAsync rather than disposing an
84
+ /// active window. Diagnostics receive complete stderr and frames belonging
85
+ /// to retired documents; errors stay outside ordinary application content.
86
+ /// Callbacks must not synchronously wait for Ready, Completion or CloseAsync.
87
+ /// </summary>
88
+ public sealed class ArcaneHostForm : Form
89
+ {
90
+ private const string CancelRendererRequests = "{\"protocol\":\"arcane/1\",\"type\":\"control\",\"control\":\"requests.cancelAll\"}";
91
+ private readonly ArcaneHostOptions options;
92
+ private readonly Action<string> onDiagnostic;
93
+ private readonly Action<Exception> onError;
94
+ private readonly WebView2 webView;
95
+ private readonly object stateLock = new object();
96
+ private readonly Queue<CoreDelivery> pending = new Queue<CoreDelivery>();
97
+ private readonly Dictionary<string, long> requestGenerations = new Dictionary<string, long>();
98
+ private readonly List<Exception> failures = new List<Exception>();
99
+ private readonly List<Task> failureNotifications = new List<Task>();
100
+ private readonly TaskCompletionSource<object> ready = new TaskCompletionSource<object>();
101
+ private readonly TaskCompletionSource<object> initialized = new TaskCompletionSource<object>();
102
+ private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
103
+ private ArcaneCoreProcess core;
104
+ private ArcaneBridge bridge;
105
+ private ArcaneBridge activeBridge;
106
+ private Task initialization;
107
+ private Task coreLifetime;
108
+ private Task shutdown;
109
+ private string injectedScript;
110
+ private Exception transportFailure;
111
+ private long generation;
112
+ private long nextGeneration;
113
+ private long registeredGeneration;
114
+ private long failureNotifiedGeneration = -1;
115
+ private ulong navigationId;
116
+ private bool started;
117
+ private bool documentReady;
118
+ private bool deliveryScheduled;
119
+ private bool closing;
120
+ private bool closeAllowed;
121
+
122
+ public ArcaneHostForm(ArcaneHostOptions options, Action<string> onDiagnostic,
123
+ Action<Exception> onError = null)
124
+ {
125
+ if (options == null) throw new ArgumentNullException("options");
126
+ if (onDiagnostic == null) throw new ArgumentNullException("onDiagnostic");
127
+ this.options = options;
128
+ this.onDiagnostic = onDiagnostic;
129
+ this.onError = onError;
130
+ ready.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
131
+ completion.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
132
+ Text = options.Title;
133
+ AutoScaleMode = AutoScaleMode.Dpi;
134
+ webView = new WebView2 { Dock = DockStyle.Fill };
135
+ Controls.Add(webView);
136
+ }
137
+
138
+ /// <summary>First document navigation completed; not model readiness.</summary>
139
+ public Task Ready { get { return ready.Task; } }
140
+ public Task Completion { get { return completion.Task; } }
141
+
142
+ protected override void OnLoad(EventArgs args)
143
+ {
144
+ base.OnLoad(args);
145
+ if (started || closing) return;
146
+ started = true;
147
+ initialization = InitializeAsync();
148
+ initialization.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
149
+ }
150
+
151
+ private async Task InitializeAsync()
152
+ {
153
+ try
154
+ {
155
+ if (String.IsNullOrWhiteSpace(options.ApplicationRoot)) throw new ArgumentException("Select the application root.");
156
+ if (String.IsNullOrWhiteSpace(options.ProfileDirectory)) throw new ArgumentException("Select the WebView2 profile directory.");
157
+ if (String.IsNullOrWhiteSpace(options.OriginHost)) throw new ArgumentException("Select the application's stable virtual origin host.");
158
+ if (String.IsNullOrEmpty(options.StartPath)) throw new ArgumentException("Select the application start path.");
159
+ if (String.IsNullOrEmpty(options.ClassicClientSource)) throw new ArgumentException("Supply the SDK-generated classic Core client.");
160
+ string applicationRoot = Path.GetFullPath(options.ApplicationRoot);
161
+ if (!Directory.Exists(applicationRoot)) throw new DirectoryNotFoundException(applicationRoot);
162
+ string profile = Path.GetFullPath(options.ProfileDirectory);
163
+ Uri origin = new UriBuilder(Uri.UriSchemeHttps, options.OriginHost).Uri;
164
+ Uri start = new Uri(origin, options.StartPath);
165
+ core = ArcaneCoreProcess.Start(options.CoreExecutable, options.CoreArguments,
166
+ options.CoreWorkingDirectory, ReceiveCoreMessage, DeliverDiagnostic, CoreFailed);
167
+ coreLifetime = ObserveCoreLifetimeAsync();
168
+ CoreWebView2Environment environment = await CoreWebView2Environment.CreateAsync(null, profile);
169
+ if (closing) return;
170
+ await webView.EnsureCoreWebView2Async(environment);
171
+ if (closing) return;
172
+ CoreWebView2 browser = webView.CoreWebView2;
173
+ browser.SetVirtualHostNameToFolderMapping(options.OriginHost, applicationRoot,
174
+ CoreWebView2HostResourceAccessKind.Allow);
175
+ InstallDocumentBridge(generation);
176
+ // The selected generator uses replayRuntimeState:true. Both RPC
177
+ // installation and the shared event owner remain SDK-owned.
178
+ injectedScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.ClassicClientSource);
179
+ if (closing) return;
180
+ browser.NavigationStarting += NavigationStarting;
181
+ browser.NavigationCompleted += NavigationCompleted;
182
+ browser.ProcessFailed += BrowserProcessFailed;
183
+ browser.Navigate(start.AbsoluteUri);
184
+ }
185
+ catch (Exception error)
186
+ {
187
+ Report(error);
188
+ ready.TrySetException(error);
189
+ BeginClose();
190
+ }
191
+ finally { initialized.TrySetResult(null); }
192
+ }
193
+
194
+ private void NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
195
+ {
196
+ if (nextGeneration != 0 && navigationId == args.NavigationId) return;
197
+ navigationId = args.NavigationId;
198
+ if (!closing)
199
+ {
200
+ // An attempted navigation can be cancelled while the original
201
+ // document survives. Its captured bridge remains usable until
202
+ // the next document's SDK client actually connects.
203
+ try { InstallDocumentBridge(++nextGeneration); }
204
+ catch (Exception error) { CoreFailed(error); }
205
+ }
206
+ }
207
+
208
+ private void InstallDocumentBridge(long documentGeneration)
209
+ {
210
+ if (bridge != null)
211
+ {
212
+ if (bridge != activeBridge)
213
+ bridge.Stop(new OperationCanceledException("The pending document navigation was replaced."));
214
+ webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
215
+ }
216
+ bridge = new ArcaneBridge(delegate(string json) { return SendDocumentMessage(documentGeneration, json); }, CoreFailed,
217
+ delegate { DocumentConnected(documentGeneration); });
218
+ registeredGeneration = documentGeneration;
219
+ if (transportFailure != null) bridge.Stop(transportFailure);
220
+ webView.CoreWebView2.AddHostObjectToScript("arcaneBridge", bridge);
221
+ }
222
+
223
+ private Task SendDocumentMessage(long documentGeneration, string json)
224
+ {
225
+ Dictionary<string, object> envelope = ReadEnvelope(json);
226
+ string requestId = EnvelopeString(envelope, "type") == "request" ? EnvelopeString(envelope, "id") : null;
227
+ lock (stateLock)
228
+ {
229
+ if (documentGeneration != generation || closing)
230
+ throw new InvalidOperationException("The requesting document is no longer connected.");
231
+ Task accepted = core.SendAsync(json);
232
+ // Hold the same lock used by the reader so an immediate response
233
+ // cannot overtake its accepted transport correlation record.
234
+ if (requestId != null && !accepted.IsFaulted && !accepted.IsCanceled)
235
+ requestGenerations[requestId] = documentGeneration;
236
+ return accepted;
237
+ }
238
+ }
239
+
240
+ private void DocumentConnected(long documentGeneration)
241
+ {
242
+ ArcaneBridge previous;
243
+ lock (stateLock)
244
+ {
245
+ if (closing) return;
246
+ if (documentGeneration == generation && documentReady) return;
247
+ if (documentGeneration != registeredGeneration) return;
248
+ previous = activeBridge;
249
+ generation = documentGeneration;
250
+ activeBridge = bridge;
251
+ documentReady = true;
252
+ }
253
+ if (previous != null && previous != activeBridge)
254
+ {
255
+ previous.Stop(new OperationCanceledException("The previous document has navigated away."));
256
+ if (transportFailure == null)
257
+ {
258
+ try { ObserveSend(core.SendAsync(CancelRendererRequests)); }
259
+ catch (Exception error) { CoreFailed(error); }
260
+ }
261
+ }
262
+ // The canonical client attaches its message listener before its
263
+ // first Send, even if the transport rejects that send. Listener
264
+ // readiness is independent of write acceptance and page load.
265
+ // WebView2 invokes its host objects on the owning UI thread.
266
+ DrainRetiredFrames();
267
+ ScheduleDelivery();
268
+ if (transportFailure != null) NotifyTransportFailure();
269
+ }
270
+
271
+ private void NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs args)
272
+ {
273
+ if (navigationId != args.NavigationId) return;
274
+ if (!args.IsSuccess)
275
+ {
276
+ if (activeBridge != null && bridge != activeBridge)
277
+ {
278
+ // No new SDK connection superseded the surviving document.
279
+ // Restore its registration without invalidating its existing
280
+ // JavaScript reference or changing its request generation.
281
+ try
282
+ {
283
+ bridge.Stop(new OperationCanceledException("The pending document navigation did not complete."));
284
+ webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
285
+ bridge = activeBridge;
286
+ registeredGeneration = generation;
287
+ webView.CoreWebView2.AddHostObjectToScript("arcaneBridge", bridge);
288
+ }
289
+ catch (Exception restoreError) { CoreFailed(restoreError); }
290
+ }
291
+ IOException error = new IOException("Application navigation failed: " + args.WebErrorStatus + ".");
292
+ Report(error);
293
+ ready.TrySetException(error);
294
+ return;
295
+ }
296
+ ready.TrySetResult(null);
297
+ }
298
+
299
+ private void BrowserProcessFailed(object sender, CoreWebView2ProcessFailedEventArgs args)
300
+ {
301
+ CoreWebView2ProcessFailedKind kind = args.ProcessFailedKind;
302
+ Exception error = new IOException("WebView2 process failed: " + kind + ".");
303
+ try
304
+ {
305
+ // Read platform properties on their UI thread while this
306
+ // notification is active. Do not retain a live COM wrapper in
307
+ // an error that will later cross the JSON transport boundary.
308
+ JavaScriptSerializer serializer = ArcaneHost.Serializer();
309
+ error.Data["webView2ProcessFailure"] = serializer.DeserializeObject(serializer.Serialize(args));
310
+ }
311
+ catch (Exception diagnosticError)
312
+ {
313
+ error = new AggregateException("Reading WebView2 process-failure diagnostics failed.", error, diagnosticError);
314
+ }
315
+ // Only loss of the browser or top-level renderer ends this bridge.
316
+ // GPU/utility/plugin helpers recover independently; subframe loss,
317
+ // unresponsiveness and unspecified failures remain full diagnostics.
318
+ if (kind == CoreWebView2ProcessFailedKind.BrowserProcessExited
319
+ || kind == CoreWebView2ProcessFailedKind.RenderProcessExited) CoreFailed(error);
320
+ else Report(error);
321
+ }
322
+
323
+ private void ReceiveCoreMessage(string json)
324
+ {
325
+ Dictionary<string, object> envelope = ReadEnvelope(json);
326
+ lock (stateLock)
327
+ {
328
+ long targetGeneration = generation;
329
+ if (EnvelopeString(envelope, "type") == "response")
330
+ {
331
+ string requestId = EnvelopeString(envelope, "id");
332
+ if (requestId == null || !requestGenerations.TryGetValue(requestId, out targetGeneration))
333
+ targetGeneration = -1;
334
+ else requestGenerations.Remove(requestId);
335
+ }
336
+ pending.Enqueue(new CoreDelivery(targetGeneration, json));
337
+ }
338
+ ScheduleDelivery();
339
+ }
340
+
341
+ private static Dictionary<string, object> ReadEnvelope(string json)
342
+ {
343
+ try { return ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>; }
344
+ catch (Exception error)
345
+ {
346
+ error.Data["coreMessage"] = json;
347
+ throw;
348
+ }
349
+ }
350
+
351
+ private static string EnvelopeString(Dictionary<string, object> envelope, string name)
352
+ {
353
+ object value;
354
+ return envelope != null && envelope.TryGetValue(name, out value) ? value as string : null;
355
+ }
356
+
357
+ private void ScheduleDelivery()
358
+ {
359
+ lock (stateLock)
360
+ {
361
+ if (deliveryScheduled || pending.Count == 0 || (!documentReady && !closing)) return;
362
+ deliveryScheduled = true;
363
+ }
364
+ PostToUi(DrainDeliveries);
365
+ }
366
+
367
+ private void DrainDeliveries()
368
+ {
369
+ while (true)
370
+ {
371
+ CoreDelivery delivery;
372
+ bool current;
373
+ lock (stateLock)
374
+ {
375
+ if (pending.Count == 0 || (!documentReady && !closing))
376
+ {
377
+ deliveryScheduled = false;
378
+ return;
379
+ }
380
+ delivery = pending.Dequeue();
381
+ current = delivery.Generation == generation && !closing && transportFailure == null;
382
+ }
383
+ if (!current) { DeliverDiagnostic(delivery.Json); continue; }
384
+ try { webView.CoreWebView2.PostWebMessageAsJson(delivery.Json); }
385
+ catch (Exception error)
386
+ {
387
+ error.Data["coreMessage"] = delivery.Json;
388
+ CoreFailed(error);
389
+ }
390
+ }
391
+ }
392
+
393
+ private void DrainRetiredFrames()
394
+ {
395
+ while (true)
396
+ {
397
+ CoreDelivery delivery;
398
+ lock (stateLock)
399
+ {
400
+ if (pending.Count == 0 || pending.Peek().Generation == generation) return;
401
+ delivery = pending.Dequeue();
402
+ }
403
+ DeliverDiagnostic(delivery.Json);
404
+ }
405
+ }
406
+
407
+ private void CoreFailed(Exception error)
408
+ {
409
+ Report(error);
410
+ lock (stateLock)
411
+ {
412
+ if (transportFailure == null) transportFailure = error;
413
+ }
414
+ PostToUi(HandleTransportFailure);
415
+ }
416
+
417
+ private void HandleTransportFailure()
418
+ {
419
+ if (IsDisposed) return;
420
+ if (bridge != null) bridge.Stop(transportFailure);
421
+ if (activeBridge != null) activeBridge.Stop(transportFailure);
422
+ NotifyTransportFailure();
423
+ if (core != null) core.CloseAsync();
424
+ ScheduleDelivery();
425
+ }
426
+
427
+ private void NotifyTransportFailure()
428
+ {
429
+ if (!documentReady || webView.CoreWebView2 == null || transportFailure == null || closing) return;
430
+ if (failureNotifiedGeneration == generation) return;
431
+ failureNotifiedGeneration = generation;
432
+ Task failureNotification = NotifyTransportFailureAsync(transportFailure);
433
+ failureNotifications.Add(failureNotification);
434
+ failureNotification.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
435
+ }
436
+
437
+ private async Task NotifyTransportFailureAsync(Exception error)
438
+ {
439
+ string record = ArcaneHost.Serializer().Serialize(ArcaneHost.ErrorRecord(error, "ARCANE_NATIVE_TRANSPORT_FAILED"));
440
+ // This callback is installed by the canonical client, which owns
441
+ // rejection of pending requests and complete transport diagnostics.
442
+ string result = await webView.CoreWebView2.ExecuteScriptAsync(
443
+ "(function(){if(typeof globalThis.__arcaneTransportFailed!=='function')return false;"
444
+ + "globalThis.__arcaneTransportFailed(" + record + ");return true;})()");
445
+ if (result != "true")
446
+ throw new InvalidOperationException("The injected SDK client has no transport-failure callback.", error);
447
+ }
448
+
449
+ private async Task ObserveCoreLifetimeAsync()
450
+ {
451
+ try { await core.Completion.ConfigureAwait(false); }
452
+ catch (Exception error) { CoreFailed(error); }
453
+ }
454
+
455
+ private void ObserveSend(Task send)
456
+ {
457
+ send.ContinueWith(ObserveSendFailure, TaskContinuationOptions.OnlyOnFaulted);
458
+ }
459
+
460
+ private void ObserveSendFailure(Task send) { CoreFailed(send.Exception); }
461
+
462
+ public Task CloseAsync()
463
+ {
464
+ PostToUi(BeginClose);
465
+ return Completion;
466
+ }
467
+
468
+ private void BeginClose()
469
+ {
470
+ if (closing) return;
471
+ lock (stateLock) closing = true;
472
+ if (!started) initialized.TrySetResult(null);
473
+ if (bridge != null) bridge.Stop(new InvalidOperationException("The application window is closing."));
474
+ if (activeBridge != null) activeBridge.Stop(new InvalidOperationException("The application window is closing."));
475
+ shutdown = DrainAndCloseAsync();
476
+ shutdown.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
477
+ }
478
+
479
+ private async Task DrainAndCloseAsync()
480
+ {
481
+ await initialized.Task;
482
+ if (core != null)
483
+ {
484
+ Task cancellation = Task.FromResult<object>(null);
485
+ try
486
+ {
487
+ if (transportFailure == null) cancellation = core.SendAsync(CancelRendererRequests);
488
+ }
489
+ catch (Exception error) { Report(error); }
490
+ try { core.CloseAsync(); }
491
+ catch (Exception error) { Report(error); }
492
+ Task settled = Task.WhenAll(cancellation, core.Completion);
493
+ try { await settled; }
494
+ catch (Exception error) { ReportTaskFailure(settled, error); }
495
+ if (coreLifetime != null)
496
+ {
497
+ try { await coreLifetime; }
498
+ catch (Exception error) { ReportTaskFailure(coreLifetime, error); }
499
+ }
500
+ }
501
+ Task notifications = Task.WhenAll(failureNotifications);
502
+ try { await notifications; }
503
+ catch (Exception error) { ReportTaskFailure(notifications, error); }
504
+ DrainDeliveries();
505
+ // Cancellation does not release correlations: an accepted service
506
+ // request can still finish after its renderer has gone away. All
507
+ // remaining IDs belong to the child lifetime that just drained.
508
+ lock (stateLock) requestGenerations.Clear();
509
+ ready.TrySetCanceled();
510
+ try
511
+ {
512
+ if (webView.CoreWebView2 != null)
513
+ {
514
+ webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
515
+ if (injectedScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(injectedScript);
516
+ }
517
+ }
518
+ catch (Exception error) { Report(error); }
519
+ closeAllowed = true;
520
+ // Defer the final Close so a synchronous drain never recursively
521
+ // closes the form inside its original FormClosing notification.
522
+ try
523
+ {
524
+ if (IsHandleCreated) BeginInvoke(new Action(CloseDrainedWindow));
525
+ else CloseDrainedWindow();
526
+ }
527
+ catch (Exception error) { Report(error); CompleteWindow(); }
528
+ }
529
+
530
+ private void CloseDrainedWindow()
531
+ {
532
+ try { Close(); }
533
+ catch (Exception error) { Report(error); CompleteWindow(); }
534
+ if (IsDisposed) CompleteWindow();
535
+ }
536
+
537
+ protected override void OnFormClosing(FormClosingEventArgs args)
538
+ {
539
+ base.OnFormClosing(args);
540
+ if (args.Cancel) return;
541
+ if (closeAllowed) return;
542
+ args.Cancel = true;
543
+ BeginClose();
544
+ }
545
+
546
+ protected override void OnFormClosed(FormClosedEventArgs args)
547
+ {
548
+ base.OnFormClosed(args);
549
+ CompleteWindow();
550
+ }
551
+
552
+ private void CompleteWindow()
553
+ {
554
+ Exception[] errors;
555
+ lock (stateLock) errors = failures.ToArray();
556
+ if (errors.Length == 0) completion.TrySetResult(null);
557
+ else completion.TrySetException(new AggregateException("The Windows Core host closed with errors.", errors));
558
+ }
559
+
560
+ private void PostToUi(Action action)
561
+ {
562
+ if (IsDisposed) return;
563
+ try
564
+ {
565
+ if (InvokeRequired) BeginInvoke(new Action(delegate { InvokeUiAction(action); }));
566
+ else InvokeUiAction(action);
567
+ }
568
+ catch (Exception error) { Report(error); }
569
+ }
570
+
571
+ private void InvokeUiAction(Action action)
572
+ {
573
+ try { action(); }
574
+ catch (Exception error) { Report(error); }
575
+ }
576
+
577
+ private void DeliverDiagnostic(string text)
578
+ {
579
+ try { onDiagnostic(text); }
580
+ catch (Exception error)
581
+ {
582
+ IOException failure = new IOException("The host diagnostic callback failed.", error);
583
+ failure.Data["diagnostic"] = text;
584
+ Report(failure);
585
+ }
586
+ }
587
+
588
+ private void Report(Exception error)
589
+ {
590
+ lock (stateLock)
591
+ {
592
+ if (failures.Contains(error)) return;
593
+ failures.Add(error);
594
+ }
595
+ if (onError == null) return;
596
+ try { onError(error); }
597
+ catch (Exception callbackError)
598
+ {
599
+ lock (stateLock) failures.Add(callbackError);
600
+ }
601
+ }
602
+
603
+ private void ObserveUnexpectedFailure(Task failed) { Report(failed.Exception); }
604
+ private static void ObserveReportedFailure(Task failed) { failed.Exception.Handle(error => true); }
605
+
606
+ private void ReportTaskFailure(Task task, Exception caught)
607
+ {
608
+ // Await reports one failure; retain every sibling task failure.
609
+ // A cancelled task has no Exception and still needs observation.
610
+ if (task.Exception == null) { Report(caught); return; }
611
+ foreach (Exception error in task.Exception.InnerExceptions) Report(error);
612
+ }
613
+
614
+ private sealed class CoreDelivery
615
+ {
616
+ internal readonly long Generation;
617
+ internal readonly string Json;
618
+ internal CoreDelivery(long generation, string json) { Generation = generation; Json = json; }
619
+ }
620
+ }
621
+
622
+ [ComVisible(true)]
623
+ [ClassInterface(ClassInterfaceType.AutoDual)]
624
+ public sealed class ArcaneBridge
625
+ {
626
+ private readonly Func<string, Task> send;
627
+ private readonly Action<Exception> onFailure;
628
+ private readonly Action onConnected;
629
+ private Exception stopped;
630
+
631
+ internal ArcaneBridge(Func<string, Task> send, Action<Exception> onFailure, Action onConnected)
632
+ {
633
+ this.send = send;
634
+ this.onFailure = onFailure;
635
+ this.onConnected = onConnected;
636
+ }
637
+
638
+ internal void Stop(Exception reason) { Interlocked.CompareExchange(ref stopped, reason, null); }
639
+
640
+ public string Send(string json)
641
+ {
642
+ try
643
+ {
644
+ // Calling Send proves the SDK listener is installed. Publish
645
+ // that boundary even when Core failed before the first write,
646
+ // so the client can observe the complete transport failure.
647
+ onConnected();
648
+ Exception unavailable = Volatile.Read(ref stopped);
649
+ if (unavailable != null) return Rejected(unavailable);
650
+ Task accepted = send(json);
651
+ if (accepted.IsFaulted)
652
+ {
653
+ Stop(accepted.Exception);
654
+ onFailure(accepted.Exception);
655
+ return Rejected(accepted.Exception);
656
+ }
657
+ if (accepted.IsCanceled)
658
+ {
659
+ OperationCanceledException error = new OperationCanceledException("The Core transport write was cancelled.");
660
+ Stop(error);
661
+ onFailure(error);
662
+ return Rejected(error);
663
+ }
664
+ accepted.ContinueWith(SendCompleted, TaskScheduler.Default);
665
+ // This acknowledges the ordered write queue, not RPC success.
666
+ return "{\"accepted\":true}";
667
+ }
668
+ catch (Exception error)
669
+ {
670
+ Stop(error);
671
+ onFailure(error);
672
+ return Rejected(error);
673
+ }
674
+ }
675
+
676
+ private void SendCompleted(Task sent)
677
+ {
678
+ if (sent.IsFaulted) { Stop(sent.Exception); onFailure(sent.Exception); }
679
+ else if (sent.IsCanceled)
680
+ {
681
+ OperationCanceledException error = new OperationCanceledException("The Core transport write was cancelled.");
682
+ Stop(error);
683
+ onFailure(error);
684
+ }
685
+ }
686
+
687
+ private static string Rejected(Exception error)
688
+ {
689
+ return ArcaneHost.Serializer().Serialize(new Dictionary<string, object>
690
+ {
691
+ { "accepted", false }, { "error", ArcaneHost.ErrorRecord(error, "ARCANE_BRIDGE_WRITE_FAILED") }
692
+ });
693
+ }
694
+ }
695
+ }