arcane-os 0.91.0 → 0.93.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.
@@ -24,6 +24,7 @@ namespace Arcane.Core.Hosts.Windows
24
24
  public string Title { get; set; }
25
25
  public string IconPath { get; set; }
26
26
  public string ClassicClientSource { get; set; }
27
+ public string NativeDiagnosticsSource { get; set; }
27
28
  public string AppControlEndpoint { get; set; }
28
29
  public string AppControlSource { get; set; }
29
30
  public string CoreExecutable { get; set; }
@@ -102,15 +103,20 @@ namespace Arcane.Core.Hosts.Windows
102
103
  private readonly Action<string> onDiagnostic;
103
104
  private readonly Action<Exception> onError;
104
105
  private readonly WebView2 webView;
106
+ private readonly ArcaneNativeDiagnostics nativeDiagnostics;
105
107
  private readonly object stateLock = new object();
106
108
  private readonly Queue<CoreDelivery> pending = new Queue<CoreDelivery>();
107
109
  private readonly Dictionary<string, long> requestGenerations = new Dictionary<string, long>();
110
+ private readonly Dictionary<string, DesktopNotificationRequest> desktopNotificationRequests =
111
+ new Dictionary<string, DesktopNotificationRequest>();
108
112
  private readonly List<Exception> failures = new List<Exception>();
109
113
  private readonly List<Task> failureNotifications = new List<Task>();
110
114
  private readonly TaskCompletionSource<object> ready = new TaskCompletionSource<object>();
111
115
  private readonly TaskCompletionSource<object> initialized = new TaskCompletionSource<object>();
112
116
  private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
113
117
  private ArcaneCoreProcess core;
118
+ private ArcaneNotifications desktopNotifications;
119
+ private Task desktopNotificationsClosing;
114
120
  private ArcaneBridge bridge;
115
121
  private ArcaneBridge activeBridge;
116
122
  private Task initialization;
@@ -119,6 +125,8 @@ namespace Arcane.Core.Hosts.Windows
119
125
  private Task iconLoading;
120
126
  private Icon applicationIcon;
121
127
  private string injectedScript;
128
+ private string nativeDiagnosticsScript;
129
+ private bool nativeDiagnosticsRegistered;
122
130
  private Exception transportFailure;
123
131
  private long generation;
124
132
  private long nextGeneration;
@@ -144,6 +152,7 @@ namespace Arcane.Core.Hosts.Windows
144
152
  this.options = options;
145
153
  this.onDiagnostic = onDiagnostic;
146
154
  this.onError = onError;
155
+ nativeDiagnostics = new ArcaneNativeDiagnostics(Report);
147
156
  ready.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
148
157
  completion.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
149
158
  Text = options.Title;
@@ -494,6 +503,10 @@ namespace Arcane.Core.Hosts.Windows
494
503
  browser.SetVirtualHostNameToFolderMapping(options.OriginHost, applicationRoot,
495
504
  CoreWebView2HostResourceAccessKind.Allow);
496
505
  InstallDocumentBridge(generation);
506
+ // Register the diagnostic observer first so failures in the
507
+ // ordinary client itself use an ingress independent of Core.
508
+ await InstallNativeDiagnosticsAsync(browser);
509
+ if (closing) return;
497
510
  // The selected generator uses replayRuntimeState:true. Both RPC
498
511
  // installation and the shared event owner remain SDK-owned.
499
512
  Task scripts = Task.WhenAll(InstallClassicClientAsync(browser), InstallAppControlAsync(browser));
@@ -523,6 +536,15 @@ namespace Arcane.Core.Hosts.Windows
523
536
  injectedScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.ClassicClientSource);
524
537
  }
525
538
 
539
+ private async Task InstallNativeDiagnosticsAsync(CoreWebView2 browser)
540
+ {
541
+ if (String.IsNullOrEmpty(options.NativeDiagnosticsSource)) return;
542
+ browser.AddHostObjectToScript("arcaneDiagnostics", nativeDiagnostics);
543
+ nativeDiagnosticsRegistered = true;
544
+ nativeDiagnosticsScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.NativeDiagnosticsSource);
545
+ nativeDiagnostics.Installed();
546
+ }
547
+
526
548
  private void NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
527
549
  {
528
550
  if (nextGeneration != 0 && navigationId == args.NavigationId) return;
@@ -561,6 +583,33 @@ namespace Arcane.Core.Hosts.Windows
561
583
  if (documentGeneration != generation || closing)
562
584
  throw new InvalidOperationException("The requesting document is no longer connected.");
563
585
  string method = EnvelopeString(envelope, "method");
586
+ if (EnvelopeString(envelope, "protocol") == "arcane/1")
587
+ {
588
+ if (requestId != null && (method == "notifications.status" || method == "notifications.show"
589
+ || method == "notifications.state" || method == "notifications.close"))
590
+ {
591
+ DesktopNotificationRequest operation = new DesktopNotificationRequest(documentGeneration);
592
+ desktopNotificationRequests.Add(requestId, operation);
593
+ operation.Completion = CompleteDesktopNotificationAsync(envelope, requestId, method, operation);
594
+ operation.Completion.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
595
+ // Bridge acceptance means this host owns the request;
596
+ // the correlated response carries its native outcome.
597
+ return Task.FromResult<object>(null);
598
+ }
599
+ if (EnvelopeString(envelope, "type") == "control")
600
+ {
601
+ string control = EnvelopeString(envelope, "control");
602
+ DesktopNotificationRequest selected;
603
+ if (control == "request.cancel" && desktopNotificationRequests.TryGetValue(
604
+ EnvelopeString(envelope, "requestId") ?? String.Empty, out selected)
605
+ && selected.Generation == documentGeneration)
606
+ {
607
+ selected.Cancellation.Cancel();
608
+ return Task.FromResult<object>(null);
609
+ }
610
+ if (control == "requests.cancelAll") CancelDesktopNotificationRequests(documentGeneration);
611
+ }
612
+ }
564
613
  if (requestId != null && EnvelopeString(envelope, "protocol") == "arcane/1"
565
614
  && (method == "window.setTheme" || method == "window.state" || method == "window.setState"))
566
615
  {
@@ -584,6 +633,115 @@ namespace Arcane.Core.Hosts.Windows
584
633
  }
585
634
  }
586
635
 
636
+ private async Task CompleteDesktopNotificationAsync(Dictionary<string, object> request,
637
+ string id, string method, DesktopNotificationRequest operation)
638
+ {
639
+ try
640
+ {
641
+ Dictionary<string, object> response = new Dictionary<string, object>
642
+ {
643
+ { "protocol", "arcane/1" }, { "type", "response" }, { "id", id }
644
+ };
645
+ try
646
+ {
647
+ object value;
648
+ Dictionary<string, object> parameters = request.TryGetValue("parameters", out value)
649
+ ? value as Dictionary<string, object> : new Dictionary<string, object>();
650
+ if (parameters == null) throw new ArgumentException("Supply a notification request object.");
651
+ if (desktopNotifications == null)
652
+ desktopNotifications = new ArcaneNotifications(options.ApplicationId, options.ProfileDirectory,
653
+ options.Title, options.IconPath, DesktopNotificationChanged);
654
+ Dictionary<string, object> result = await desktopNotifications.InvokeAsync(
655
+ method, parameters, operation.Cancellation.Token).ConfigureAwait(false);
656
+ // Cancellation after native submission does not rewrite an
657
+ // accepted result. Its record also remains in host state.
658
+ response["ok"] = true;
659
+ response["result"] = result;
660
+ }
661
+ catch (Exception error)
662
+ {
663
+ string code = error.Data["code"] as string;
664
+ if (code == null) code = error is OperationCanceledException ? "CANCELLED"
665
+ : error is ArgumentException ? "INVALID_ARGUMENT" : "ARCANE_NOTIFICATION_FAILED";
666
+ response["ok"] = false;
667
+ response["error"] = ArcaneHost.ErrorRecord(error, code);
668
+ }
669
+ response["time"] = DateTime.UtcNow.ToString("o");
670
+ string json = ArcaneHost.Serializer().Serialize(response);
671
+ lock (stateLock) pending.Enqueue(new CoreDelivery(operation.Generation, json));
672
+ ScheduleDelivery();
673
+ }
674
+ catch (Exception error)
675
+ {
676
+ // A bridge-accepted request must receive its response or the
677
+ // owning transport failure, including serialization failures.
678
+ CoreFailed(error);
679
+ }
680
+ finally
681
+ {
682
+ lock (stateLock)
683
+ {
684
+ desktopNotificationRequests.Remove(id);
685
+ operation.Cancellation.Dispose();
686
+ }
687
+ }
688
+ }
689
+
690
+ private void DesktopNotificationChanged(Dictionary<string, object> record)
691
+ {
692
+ string json = ArcaneHost.Serializer().Serialize(new Dictionary<string, object>
693
+ {
694
+ { "protocol", "arcane/1" }, { "type", "event" },
695
+ { "event", "notifications.state" }, { "data", record }, { "time", DateTime.UtcNow.ToString("o") }
696
+ });
697
+ lock (stateLock) pending.Enqueue(new CoreDelivery(generation, json));
698
+ ScheduleDelivery();
699
+ }
700
+
701
+ private void CancelDesktopNotificationRequests(long? documentGeneration)
702
+ {
703
+ lock (stateLock)
704
+ {
705
+ // Cancellation can settle a queued operation synchronously;
706
+ // iterate a snapshot while its completion removes correlation.
707
+ foreach (DesktopNotificationRequest operation in
708
+ new List<DesktopNotificationRequest>(desktopNotificationRequests.Values))
709
+ if (!documentGeneration.HasValue || operation.Generation == documentGeneration.Value)
710
+ operation.Cancellation.Cancel();
711
+ }
712
+ }
713
+
714
+ private void StopDesktopNotifications()
715
+ {
716
+ CancelDesktopNotificationRequests(null);
717
+ if (desktopNotifications == null || desktopNotificationsClosing != null) return;
718
+ try { desktopNotificationsClosing = desktopNotifications.CloseAsync(); }
719
+ catch (Exception error) { Report(error); }
720
+ }
721
+
722
+ private async Task DrainDesktopNotificationsAsync()
723
+ {
724
+ List<Task> operations = new List<Task>();
725
+ lock (stateLock)
726
+ {
727
+ foreach (DesktopNotificationRequest operation in desktopNotificationRequests.Values)
728
+ operations.Add(operation.Completion);
729
+ }
730
+ if (desktopNotificationsClosing != null) operations.Add(desktopNotificationsClosing);
731
+ Task settled = Task.WhenAll(operations);
732
+ try { await settled; }
733
+ catch (Exception error) { ReportTaskFailure(settled, error); }
734
+ }
735
+
736
+ private sealed class DesktopNotificationRequest
737
+ {
738
+ internal readonly long Generation;
739
+ internal readonly CancellationTokenSource Cancellation = new CancellationTokenSource();
740
+ internal Task Completion;
741
+
742
+ internal DesktopNotificationRequest(long generation) { Generation = generation; }
743
+ }
744
+
587
745
  private async Task LoadApplicationIconAsync()
588
746
  {
589
747
  if (String.IsNullOrEmpty(options.IconPath)) return;
@@ -762,12 +920,14 @@ namespace Arcane.Core.Hosts.Windows
762
920
  private void DocumentConnected(long documentGeneration)
763
921
  {
764
922
  ArcaneBridge previous;
923
+ long previousGeneration;
765
924
  lock (stateLock)
766
925
  {
767
926
  if (closing) return;
768
927
  if (documentGeneration == generation && documentReady) return;
769
928
  if (documentGeneration != registeredGeneration) return;
770
929
  previous = activeBridge;
930
+ previousGeneration = generation;
771
931
  generation = documentGeneration;
772
932
  activeBridge = bridge;
773
933
  documentReady = true;
@@ -775,6 +935,7 @@ namespace Arcane.Core.Hosts.Windows
775
935
  if (previous != null && previous != activeBridge)
776
936
  {
777
937
  previous.Stop(new OperationCanceledException("The previous document has navigated away."));
938
+ CancelDesktopNotificationRequests(previousGeneration);
778
939
  if (transportFailure == null)
779
940
  {
780
941
  try { ObserveSend(core.SendAsync(CancelRendererRequests)); }
@@ -960,6 +1121,7 @@ namespace Arcane.Core.Hosts.Windows
960
1121
  if (IsDisposed) return;
961
1122
  if (bridge != null) bridge.Stop(transportFailure);
962
1123
  if (activeBridge != null) activeBridge.Stop(transportFailure);
1124
+ CancelDesktopNotificationRequests(null);
963
1125
  NotifyTransportFailure();
964
1126
  if (core != null) core.CloseAsync();
965
1127
  ScheduleDelivery();
@@ -1010,10 +1172,12 @@ namespace Arcane.Core.Hosts.Windows
1010
1172
  {
1011
1173
  if (closing) return;
1012
1174
  lock (stateLock) closing = true;
1175
+ nativeDiagnostics.Stop();
1013
1176
  if (!started) initialized.TrySetResult(null);
1014
1177
  if (bridge != null) bridge.Stop(new InvalidOperationException("The application window is closing."));
1015
1178
  if (activeBridge != null) activeBridge.Stop(new InvalidOperationException("The application window is closing."));
1016
1179
  StopAppControl();
1180
+ StopDesktopNotifications();
1017
1181
  WindowStateTransition.ReleasePending(this);
1018
1182
  shutdown = DrainAndCloseAsync();
1019
1183
  shutdown.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
@@ -1042,6 +1206,7 @@ namespace Arcane.Core.Hosts.Windows
1042
1206
  }
1043
1207
  }
1044
1208
  await DrainAppControlAsync();
1209
+ await DrainDesktopNotificationsAsync();
1045
1210
  // Joining the independent image owner belongs after Core input has
1046
1211
  // closed; image I/O must never delay accepted service work draining.
1047
1212
  if (iconLoading != null)
@@ -1069,6 +1234,8 @@ namespace Arcane.Core.Hosts.Windows
1069
1234
  if (!browserProcessExited && webView.CoreWebView2 != null)
1070
1235
  {
1071
1236
  webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
1237
+ if (nativeDiagnosticsRegistered) webView.CoreWebView2.RemoveHostObjectFromScript("arcaneDiagnostics");
1238
+ if (nativeDiagnosticsScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(nativeDiagnosticsScript);
1072
1239
  if (injectedScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(injectedScript);
1073
1240
  if (appControlScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(appControlScript);
1074
1241
  }
@@ -101,6 +101,7 @@ namespace Arcane.Core.Hosts.Windows
101
101
  InitialWindowState = OptionalWindowState(windowOptions),
102
102
  IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
103
103
  ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
104
+ NativeDiagnosticsSource = File.ReadAllText(Path.Combine(directory, "arcane-native-diagnostics.js")),
104
105
  AppControlEndpoint = appControl == null ? null : RequiredString(appControl, "endpoint"),
105
106
  AppControlSource = appControl == null ? null : File.ReadAllText(Path.Combine(directory, "arcane-app-control.js")),
106
107
  CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
@@ -0,0 +1,137 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.Runtime.InteropServices;
5
+
6
+ namespace Arcane.Core.Hosts.Windows
7
+ {
8
+ /// <summary>
9
+ /// One host-session ledger, independent of Core and renderer readiness.
10
+ /// The dedicated document-start ingress acknowledges only retained records.
11
+ /// Diagnostic property graphs are data; the host never evaluates their fields.
12
+ /// </summary>
13
+ [ComVisible(true)]
14
+ [ClassInterface(ClassInterfaceType.AutoDual)]
15
+ public sealed class ArcaneNativeDiagnostics
16
+ {
17
+ private readonly object stateLock = new object();
18
+ private readonly string sessionId = Guid.NewGuid().ToString();
19
+ private readonly string captureStartedAt = DateTime.UtcNow.ToString("o");
20
+ private readonly List<Dictionary<string, object>> records = new List<Dictionary<string, object>>();
21
+ private readonly Dictionary<string, Dictionary<string, object>> documents =
22
+ new Dictionary<string, Dictionary<string, object>>();
23
+ private readonly Action<Exception> onFailure;
24
+ private long sequence;
25
+ private bool captureInstalled;
26
+ private bool stopped;
27
+
28
+ internal ArcaneNativeDiagnostics(Action<Exception> onFailure) { this.onFailure = onFailure; }
29
+
30
+ public string Send(string json)
31
+ {
32
+ try
33
+ {
34
+ Dictionary<string, object> frame = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
35
+ if (frame == null || Text(frame, "protocol") != "arcane.native-diagnostics/1")
36
+ throw new InvalidDataException("Expected an Arcane native diagnostic envelope.");
37
+ string type = Text(frame, "type");
38
+ string documentId = Text(frame, "documentId");
39
+ string documentUrl = Text(frame, "documentUrl");
40
+ string time = Text(frame, "time");
41
+ if (documentId == null || documentUrl == null || time == null)
42
+ throw new InvalidDataException("The diagnostic envelope needs its document identity, URL and time.");
43
+ object value;
44
+ Dictionary<string, object> record = frame.TryGetValue("record", out value)
45
+ ? value as Dictionary<string, object> : null;
46
+ if (type != "document" && (type != "error" || record == null))
47
+ throw new InvalidDataException("Expected a document announcement or an error record.");
48
+
49
+ Dictionary<string, object> result = new Dictionary<string, object>
50
+ {
51
+ { "accepted", true }, { "sessionId", sessionId }
52
+ };
53
+ lock (stateLock)
54
+ {
55
+ if (stopped) throw new InvalidOperationException("The native diagnostic session is closing.");
56
+ string receivedAt = DateTime.UtcNow.ToString("o");
57
+ if (!documents.ContainsKey(documentId))
58
+ documents.Add(documentId, new Dictionary<string, object>
59
+ {
60
+ { "documentId", documentId }, { "documentUrl", documentUrl },
61
+ { "time", time }, { "receivedAt", receivedAt }
62
+ });
63
+ if (type == "error")
64
+ {
65
+ Dictionary<string, object> retained = new Dictionary<string, object>(record);
66
+ retained["sequence"] = ++sequence;
67
+ retained["receivedAt"] = receivedAt;
68
+ retained["documentId"] = documentId;
69
+ retained["documentUrl"] = documentUrl;
70
+ retained["time"] = time;
71
+ records.Add(retained);
72
+ result["sequence"] = sequence;
73
+ }
74
+ }
75
+ return ArcaneHost.Serializer().Serialize(result);
76
+ }
77
+ catch (Exception error)
78
+ {
79
+ onFailure(error);
80
+ return ArcaneHost.Serializer().Serialize(new Dictionary<string, object>
81
+ {
82
+ { "accepted", false },
83
+ { "error", ArcaneHost.ErrorRecord(error, "ARCANE_NATIVE_DIAGNOSTIC_CAPTURE_FAILED") }
84
+ });
85
+ }
86
+ }
87
+
88
+ internal object Snapshot(Dictionary<string, object> parameters)
89
+ {
90
+ object selected;
91
+ long afterSequence = 0;
92
+ if (parameters.TryGetValue("afterSequence", out selected))
93
+ {
94
+ if (!(selected is int || selected is long || selected is decimal || selected is double))
95
+ throw new ArgumentException("afterSequence must be a nonnegative integer.");
96
+ afterSequence = Convert.ToInt64(selected);
97
+ if (afterSequence < 0 || Convert.ToDouble(selected) != afterSequence)
98
+ throw new ArgumentException("afterSequence must be a nonnegative integer.");
99
+ }
100
+ string documentId = null;
101
+ if (parameters.TryGetValue("documentId", out selected))
102
+ {
103
+ documentId = selected as string;
104
+ if (documentId == null) throw new ArgumentException("documentId must be a string.");
105
+ }
106
+ lock (stateLock)
107
+ {
108
+ List<object> found = new List<object>();
109
+ foreach (Dictionary<string, object> record in records)
110
+ {
111
+ if ((long)record["sequence"] <= afterSequence) continue;
112
+ if (documentId != null && (string)record["documentId"] != documentId) continue;
113
+ found.Add(record);
114
+ }
115
+ List<object> observedDocuments = new List<object>();
116
+ foreach (KeyValuePair<string, Dictionary<string, object>> document in documents)
117
+ if (documentId == null || document.Key == documentId) observedDocuments.Add(document.Value);
118
+ return new Dictionary<string, object>
119
+ {
120
+ { "sessionId", sessionId }, { "captureStartedAt", captureStartedAt },
121
+ { "captureInstalled", captureInstalled }, { "latestSequence", sequence },
122
+ { "documents", observedDocuments }, { "records", found }
123
+ };
124
+ }
125
+ }
126
+
127
+ internal void Installed() { lock (stateLock) captureInstalled = true; }
128
+
129
+ internal void Stop() { lock (stateLock) stopped = true; }
130
+
131
+ private static string Text(Dictionary<string, object> record, string key)
132
+ {
133
+ object value;
134
+ return record.TryGetValue(key, out value) ? value as string : null;
135
+ }
136
+ }
137
+ }