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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +18 -0
- package/browser-runtime/core/contracts.mjs +20 -0
- package/browser-runtime/core/native-diagnostics.mjs +217 -0
- package/docs/reference/core/arcane-api.md +32 -0
- package/docs/reference/core/arcane-events.md +25 -0
- package/docs/reference/core-client.md +99 -0
- package/docs/reference/core-repositories.md +87 -4
- package/docs/reference/git-text-snapshot.md +47 -3
- package/docs/reference/inventory/package-api.json +7 -7
- package/docs/reference/native-diagnostics.md +167 -0
- package/docs/reference/sdk-api.md +52 -7
- package/package.json +3 -1
- package/src/core/app-control.mjs +1 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +1 -1
- package/src/core/hosts/windows/ArcaneHost.cs +167 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +1 -0
- package/src/core/hosts/windows/ArcaneNativeDiagnostics.cs +137 -0
- package/src/core/hosts/windows/ArcaneNotifications.cs +822 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +3 -2
- package/src/core/repositories.mjs +60 -3
- package/src/core-native-diagnostics-source.mjs +13 -0
- package/src/git-text-snapshot.mjs +25 -5
- package/src/import-map.mjs +1 -0
- package/src/process.mjs +25 -1
- package/src/repository.mjs +25 -1
- package/src/windows-git-executable.mjs +103 -0
|
@@ -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
|
+
}
|