arcane-os 0.92.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 +17 -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/inventory/package-api.json +3 -3
- package/docs/reference/native-diagnostics.md +167 -0
- 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-native-diagnostics-source.mjs +13 -0
- package/src/import-map.mjs +1 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Native application diagnostics
|
|
2
|
+
|
|
3
|
+
Native diagnostics retain errors from the actual packaged application's top-level
|
|
4
|
+
document in its native host session. Read them through the existing app-control
|
|
5
|
+
connection, without foregrounding a window, desktop input, or arbitrary script
|
|
6
|
+
execution. This is a developer inspection surface, separate from ordinary product
|
|
7
|
+
status and saved conversation history.
|
|
8
|
+
|
|
9
|
+
The Windows WebView2 host installs the fixed observer at document creation, before
|
|
10
|
+
the ordinary Core client and application scripts. It captures `window.error` and
|
|
11
|
+
`unhandledrejection` events and accepts explicit reports of application-caught
|
|
12
|
+
errors. It leaves the browser's ordinary error behavior intact. It does not
|
|
13
|
+
intercept console output, inspect application state, collect resource/request
|
|
14
|
+
bodies, or capture errors from other applications or frame documents.
|
|
15
|
+
|
|
16
|
+
## Report a caught application error
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import {reportNativeError} from 'arcane-os/core/native-diagnostics';
|
|
20
|
+
|
|
21
|
+
const receipt = await reportNativeError(error, {
|
|
22
|
+
details: {operation: 'source.load', sourceId}
|
|
23
|
+
});
|
|
24
|
+
// receipt: {accepted: true, sessionId, sequence}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`error` is the actual caught value, including non-Error rejection values.
|
|
28
|
+
`details` is optional application-selected diagnostic context. Supply diagnostic
|
|
29
|
+
information only; credentials, secrets, user documents and provider/tool payloads
|
|
30
|
+
do not belong in this report. The SDK adds document/session metadata outside the
|
|
31
|
+
reported values and does not rewrite their text. Reporting does not change the
|
|
32
|
+
application's own handling of the original failure.
|
|
33
|
+
|
|
34
|
+
The promise resolves after the native host retains the record. A missing document
|
|
35
|
+
observer or native handler rejects with
|
|
36
|
+
`ARCANE_NATIVE_DIAGNOSTICS_UNAVAILABLE`. A negative host acknowledgement rejects
|
|
37
|
+
with `ARCANE_NATIVE_DIAGNOSTIC_REJECTED`, preserving the complete host reply in
|
|
38
|
+
`details` and its error in `cause`. Reflection, encoding and native transport
|
|
39
|
+
failures reject with their actual error. A lost reply can leave the caller's
|
|
40
|
+
outcome unknown; the SDK does not automatically retry and duplicate the report.
|
|
41
|
+
|
|
42
|
+
Automatic capture observes its own reporting failures through the existing
|
|
43
|
+
console, with the original error, without recursively manufacturing an unhandled
|
|
44
|
+
rejection. Console output remains ordinary console output; it is not retained
|
|
45
|
+
diagnostic history. Applications must observe explicit reporting promises.
|
|
46
|
+
|
|
47
|
+
## Retrieve retained errors
|
|
48
|
+
|
|
49
|
+
Use the endpoint of the selected running app from its existing app-control launch
|
|
50
|
+
configuration. Connecting does not launch the application.
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import {connectAppControl} from 'arcane-os/core/app-control';
|
|
54
|
+
|
|
55
|
+
const control = await connectAppControl({endpoint});
|
|
56
|
+
try {
|
|
57
|
+
const snapshot = await control.diagnostics();
|
|
58
|
+
console.dir(snapshot, {depth: null});
|
|
59
|
+
} finally {
|
|
60
|
+
await control.close();
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`control.diagnostics({afterSequence?, documentId?}, {signal?})` invokes
|
|
65
|
+
`app.control.diagnostics`. Omitted filters return every retained error.
|
|
66
|
+
`afterSequence` selects records with a greater session sequence;
|
|
67
|
+
`documentId` selects one actual document. Retrieval is non-consuming: independent
|
|
68
|
+
inspectors can read the same complete records. Connection/cancellation behavior is
|
|
69
|
+
the existing app-control contract. Retrieval does not wait for renderer, Core,
|
|
70
|
+
model or application readiness and does not evaluate a script in the document.
|
|
71
|
+
|
|
72
|
+
The result is:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
{
|
|
76
|
+
sessionId,
|
|
77
|
+
captureStartedAt,
|
|
78
|
+
captureInstalled,
|
|
79
|
+
latestSequence,
|
|
80
|
+
documents: [{documentId, documentUrl, time, receivedAt}],
|
|
81
|
+
records: [{
|
|
82
|
+
sequence, receivedAt, documentId, documentUrl, time,
|
|
83
|
+
kind, error, details, valueFormat, values, location, message
|
|
84
|
+
}]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`sessionId` identifies this native window/host lifetime. `captureStartedAt` is the
|
|
89
|
+
time its in-memory ledger was created. `captureInstalled` means document-start
|
|
90
|
+
script registration completed; it does not prove that any application failure
|
|
91
|
+
occurred or that a particular operation succeeded. `documents` records actual
|
|
92
|
+
ingress from observed documents. Each document has one identity, shared with app
|
|
93
|
+
control, independent of Core bridge generations. A cancelled navigation that
|
|
94
|
+
leaves the same document alive keeps that identity.
|
|
95
|
+
|
|
96
|
+
`sequence` is assigned when an error is retained, in host arrival order.
|
|
97
|
+
`latestSequence` describes the whole session even when filters select fewer
|
|
98
|
+
records. `time` comes from document capture; `receivedAt` comes from the host.
|
|
99
|
+
`kind` is `window.error`, `unhandledrejection`, or `application`. Window errors
|
|
100
|
+
include `location: {file, line, column}` when the browser exposes those fields.
|
|
101
|
+
Their browser `message` is retained separately from `error`, preserving a null
|
|
102
|
+
or undefined error value rather than replacing it with that message.
|
|
103
|
+
`details` and `location` are otherwise omitted.
|
|
104
|
+
|
|
105
|
+
Records survive document navigation and Core transport failure within the host
|
|
106
|
+
session. Closing the inspector leaves them retained. Closing the native host ends
|
|
107
|
+
the session; this API does not persist, migrate, prune, or recover records across
|
|
108
|
+
host restarts. An empty snapshot means no matching records were retained in this
|
|
109
|
+
session, not that earlier failures never occurred. Errors before capture existed,
|
|
110
|
+
handled failures never explicitly reported, errors the browser does not expose,
|
|
111
|
+
and vanished documents whose pending reports never reached the host remain
|
|
112
|
+
unavailable. Earlier never-retained failures cannot be reconstructed through this API.
|
|
113
|
+
|
|
114
|
+
## Complete error property representation
|
|
115
|
+
|
|
116
|
+
Each record uses `valueFormat: 'arcane.diagnostic-value/1'`. `error` and optional
|
|
117
|
+
`details` are roots into the single `values` graph. JSON-native primitives remain
|
|
118
|
+
their original values. Undefined, bigint, non-finite numbers and negative zero
|
|
119
|
+
have explicit typed representations. `{ref: id}` identifies a graph node.
|
|
120
|
+
Repeated references and cycles retain the same identity within the record.
|
|
121
|
+
|
|
122
|
+
Object nodes retain every own property descriptor, including non-enumerable and
|
|
123
|
+
symbol-keyed fields. This includes full Error messages and stacks, `cause`,
|
|
124
|
+
AggregateError members and custom diagnostic details. Standard error fields
|
|
125
|
+
inherited from a prototype appear in `inherited`. Data descriptors contain
|
|
126
|
+
`value`; accessor descriptors contain references for `get` and `set` without
|
|
127
|
+
invoking either function. An accessor supplied as the `details` option is likewise
|
|
128
|
+
represented rather than called. No `toJSON` method on reported content is called.
|
|
129
|
+
Maps and sets retain their entries, dates their numeric time, and regular
|
|
130
|
+
expressions their source and flags, alongside their own properties.
|
|
131
|
+
|
|
132
|
+
This is a diagnostic property representation, not an executable object snapshot:
|
|
133
|
+
functions retain their property identity, not executable closures; platform-private
|
|
134
|
+
internal state and private class fields are not observable properties. The host
|
|
135
|
+
stores and returns the graph without reconstructing or executing those objects.
|
|
136
|
+
An object that rejects reflection causes an observable reporting failure rather
|
|
137
|
+
than a silently incomplete success. Inspect the complete graph in developer
|
|
138
|
+
diagnostics; do not flatten it into ordinary chat or user-facing status.
|
|
139
|
+
|
|
140
|
+
## Host ownership and availability
|
|
141
|
+
|
|
142
|
+
`installNativeDiagnostics(globalThis)` installs one observer per document and
|
|
143
|
+
returns its owner `{documentId, ready, report}`. `ready` resolves after the document
|
|
144
|
+
announcement is accepted, and `report` has the same report contract. Importing
|
|
145
|
+
the ESM module alone installs no observer. The packaged native host owns
|
|
146
|
+
installation and lifetime; ordinary applications use `reportNativeError`.
|
|
147
|
+
|
|
148
|
+
`createNativeDiagnosticsSource()` from
|
|
149
|
+
`arcane-os/core/native-diagnostics-source` projects that same canonical helper
|
|
150
|
+
into the top-level document-start script. It adds no second observer or serializer.
|
|
151
|
+
Windows binds the script to the dedicated `arcaneDiagnostics.Send` host object.
|
|
152
|
+
This route is independent of `arcaneBridge` and the Core subprocess. Host close
|
|
153
|
+
stops new ingress and removes the object/script during existing shutdown.
|
|
154
|
+
|
|
155
|
+
The implementation here supplies Windows WebView2 capture and app-control
|
|
156
|
+
retrieval. macOS, Linux and Android require their native host ingress/session and
|
|
157
|
+
app-control adapters; this source change does not claim those adapters exist.
|
|
158
|
+
The helper recognizes the same fixed-name WebKit reply handler when a host
|
|
159
|
+
provides it; that transport seam is not platform availability. A browser preview
|
|
160
|
+
cannot establish actual native acceptance.
|
|
161
|
+
|
|
162
|
+
Native delivery requires the corresponding rebuilt SDK Windows host asset and
|
|
163
|
+
consumer adoption through the published SDK package. Source inspection, authored
|
|
164
|
+
fixtures and configured capture are distinct from executed built-application
|
|
165
|
+
verification. Existing host stderr/log diagnostics, live Core events, readiness
|
|
166
|
+
replay and `Arcane.diagnostics.recentErrors/get` retain their existing semantics;
|
|
167
|
+
this API does not turn them into a historical error service.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.93.0",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
"./core/stdio": "./src/core/stdio.mjs",
|
|
66
66
|
"./core/host": "./src/core/host.mjs",
|
|
67
67
|
"./core/app-control": "./src/core/app-control.mjs",
|
|
68
|
+
"./core/native-diagnostics": "./browser-runtime/core/native-diagnostics.mjs",
|
|
69
|
+
"./core/native-diagnostics-source": "./src/core-native-diagnostics-source.mjs",
|
|
68
70
|
"./codex/app-server": "./src/codex/app-server.mjs",
|
|
69
71
|
"./mcp/stdio": "./src/mcp/stdio-server.mjs",
|
|
70
72
|
"./core/repositories": "./src/core/repositories.mjs",
|
package/src/core/app-control.mjs
CHANGED
|
@@ -15,6 +15,7 @@ export async function connectAppControl({endpoint, signal, onError = reportAppCo
|
|
|
15
15
|
endpoint,
|
|
16
16
|
closed: connection.closed,
|
|
17
17
|
status(options) { return client.invoke('app.control.status', {}, options); },
|
|
18
|
+
diagnostics(parameters = {}, options) { return client.invoke('app.control.diagnostics', parameters, options); },
|
|
18
19
|
inspect(parameters = {}, options) { return client.invoke('app.control.inspect', parameters, options); },
|
|
19
20
|
capture(parameters = {}, options) { return client.invoke('app.control.capture', parameters, options); },
|
|
20
21
|
act(parameters, options) { return client.invoke('app.control.act', parameters, options); },
|
|
@@ -452,7 +452,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
452
452
|
try
|
|
453
453
|
{
|
|
454
454
|
cancellation.Token.ThrowIfCancellationRequested();
|
|
455
|
-
if (method != "app.control.status" && method != "app.control.inspect"
|
|
455
|
+
if (method != "app.control.status" && method != "app.control.diagnostics" && method != "app.control.inspect"
|
|
456
456
|
&& method != "app.control.capture" && method != "app.control.act"
|
|
457
457
|
&& method != "app.control.key" && method != "app.control.resize")
|
|
458
458
|
throw new NotSupportedException("The app does not expose " + method + ".");
|
|
@@ -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
|
+
}
|