arcane-os 0.81.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ using System;
2
+ using System.Globalization;
3
+ using System.IO;
4
+ using System.Text;
5
+ using System.Threading.Tasks;
6
+
7
+ namespace Arcane.Core.Hosts.Windows
8
+ {
9
+ /// <summary>The shared native transport framing; callers own stream lifetime and ordered writes.</summary>
10
+ internal static class ArcaneFrameTransport
11
+ {
12
+ private static readonly Encoding Utf8 = new UTF8Encoding(false, true);
13
+ private static readonly Encoding Ascii = Encoding.GetEncoding(
14
+ "us-ascii", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
15
+
16
+ internal static async Task WriteAsync(Stream stream, string json)
17
+ {
18
+ byte[] body = Utf8.GetBytes(json);
19
+ // Length belongs only to the Content-Length transport frame.
20
+ byte[] header = Ascii.GetBytes("Content-Length: " + body.Length.ToString(CultureInfo.InvariantCulture) + "\r\n\r\n");
21
+ await stream.WriteAsync(header, 0, header.Length).ConfigureAwait(false);
22
+ await stream.WriteAsync(body, 0, body.Length).ConfigureAwait(false);
23
+ await stream.FlushAsync().ConfigureAwait(false);
24
+ }
25
+
26
+ internal static async Task<string> ReadAsync(Stream stream)
27
+ {
28
+ using (MemoryStream header = new MemoryStream())
29
+ using (MemoryStream body = new MemoryStream())
30
+ {
31
+ try
32
+ {
33
+ byte[] next = new byte[1];
34
+ byte[] separator = new byte[] { 13, 10, 13, 10 };
35
+ int matched = 0;
36
+ while (matched != separator.Length)
37
+ {
38
+ int read = await stream.ReadAsync(next, 0, 1).ConfigureAwait(false);
39
+ if (read == 0)
40
+ {
41
+ if (header.Length == 0) return null;
42
+ throw new EndOfStreamException("Core transport ended during a frame header.");
43
+ }
44
+ header.WriteByte(next[0]);
45
+ matched = next[0] == separator[matched] ? matched + 1 : next[0] == 13 ? 1 : 0;
46
+ }
47
+ byte[] headerBytes = header.ToArray();
48
+ string headerText = Ascii.GetString(headerBytes, 0, headerBytes.Length - separator.Length);
49
+ long? contentLength = null;
50
+ foreach (string line in headerText.Split(new string[] { "\r\n" }, StringSplitOptions.None))
51
+ {
52
+ if (!line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) continue;
53
+ long length;
54
+ if (contentLength.HasValue || !Int64.TryParse(line.Substring(15).Trim(), NumberStyles.None,
55
+ CultureInfo.InvariantCulture, out length))
56
+ throw new InvalidDataException("Core frame Content-Length is invalid.");
57
+ contentLength = length;
58
+ }
59
+ if (!contentLength.HasValue) throw new InvalidDataException("Core frame Content-Length is missing.");
60
+ long remaining = contentLength.Value;
61
+ byte[] buffer = new byte[8192];
62
+ while (remaining != 0)
63
+ {
64
+ int read = await stream.ReadAsync(buffer, 0, (int)Math.Min(remaining, buffer.Length)).ConfigureAwait(false);
65
+ if (read == 0) throw new EndOfStreamException("Core transport ended during a frame body.");
66
+ body.Write(buffer, 0, read);
67
+ remaining -= read;
68
+ }
69
+ return Utf8.GetString(body.ToArray());
70
+ }
71
+ catch (Exception error)
72
+ {
73
+ error.Data["frameHeader"] = header.ToArray();
74
+ error.Data["frameBody"] = body.ToArray();
75
+ throw;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
@@ -16,6 +16,7 @@ namespace Arcane.Core.Hosts.Windows
16
16
  /// <summary>Selected by the application launcher; no OS layout is inferred.</summary>
17
17
  public sealed class ArcaneHostOptions
18
18
  {
19
+ public string ApplicationId { get; set; }
19
20
  public string ApplicationRoot { get; set; }
20
21
  public string StartPath { get; set; }
21
22
  public string OriginHost { get; set; }
@@ -23,6 +24,8 @@ namespace Arcane.Core.Hosts.Windows
23
24
  public string Title { get; set; }
24
25
  public string IconPath { get; set; }
25
26
  public string ClassicClientSource { get; set; }
27
+ public string AppControlEndpoint { get; set; }
28
+ public string AppControlSource { get; set; }
26
29
  public string CoreExecutable { get; set; }
27
30
  public string CoreArguments { get; set; }
28
31
  public string CoreWorkingDirectory { get; set; }
@@ -91,7 +94,7 @@ namespace Arcane.Core.Hosts.Windows
91
94
  /// to retired documents; errors stay outside ordinary application content.
92
95
  /// Callbacks must not synchronously wait for Ready, Completion or CloseAsync.
93
96
  /// </summary>
94
- public sealed class ArcaneHostForm : Form
97
+ public sealed partial class ArcaneHostForm : Form
95
98
  {
96
99
  private const string CancelRendererRequests = "{\"protocol\":\"arcane/1\",\"type\":\"control\",\"control\":\"requests.cancelAll\"}";
97
100
  private readonly ArcaneHostOptions options;
@@ -213,6 +216,7 @@ namespace Arcane.Core.Hosts.Windows
213
216
  string profile = Path.GetFullPath(options.ProfileDirectory);
214
217
  Uri origin = new UriBuilder(Uri.UriSchemeHttps, options.OriginHost).Uri;
215
218
  Uri start = new Uri(origin, options.StartPath);
219
+ StartAppControl();
216
220
  iconLoading = LoadApplicationIconAsync();
217
221
  iconLoading.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
218
222
  core = ArcaneCoreProcess.Start(options.CoreExecutable, options.CoreArguments,
@@ -228,7 +232,13 @@ namespace Arcane.Core.Hosts.Windows
228
232
  InstallDocumentBridge(generation);
229
233
  // The selected generator uses replayRuntimeState:true. Both RPC
230
234
  // installation and the shared event owner remain SDK-owned.
231
- injectedScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.ClassicClientSource);
235
+ Task scripts = Task.WhenAll(InstallClassicClientAsync(browser), InstallAppControlAsync(browser));
236
+ try { await scripts; }
237
+ catch
238
+ {
239
+ if (scripts.Exception != null) throw scripts.Exception;
240
+ throw;
241
+ }
232
242
  if (closing) return;
233
243
  browser.NavigationStarting += NavigationStarting;
234
244
  browser.NavigationCompleted += NavigationCompleted;
@@ -244,6 +254,11 @@ namespace Arcane.Core.Hosts.Windows
244
254
  finally { initialized.TrySetResult(null); }
245
255
  }
246
256
 
257
+ private async Task InstallClassicClientAsync(CoreWebView2 browser)
258
+ {
259
+ injectedScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.ClassicClientSource);
260
+ }
261
+
247
262
  private void NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
248
263
  {
249
264
  if (nextGeneration != 0 && navigationId == args.NavigationId) return;
@@ -571,7 +586,11 @@ namespace Arcane.Core.Hosts.Windows
571
586
  // Loss of the top-level renderer ends its bridge.
572
587
  // GPU/utility/plugin helpers recover independently; subframe loss,
573
588
  // unresponsiveness and unspecified failures remain full diagnostics.
574
- if (kind == CoreWebView2ProcessFailedKind.RenderProcessExited) CoreFailed(error);
589
+ if (kind == CoreWebView2ProcessFailedKind.RenderProcessExited)
590
+ {
591
+ AppControlRendererExited();
592
+ CoreFailed(error);
593
+ }
575
594
  else Report(error);
576
595
  }
577
596
 
@@ -727,6 +746,7 @@ namespace Arcane.Core.Hosts.Windows
727
746
  if (!started) initialized.TrySetResult(null);
728
747
  if (bridge != null) bridge.Stop(new InvalidOperationException("The application window is closing."));
729
748
  if (activeBridge != null) activeBridge.Stop(new InvalidOperationException("The application window is closing."));
749
+ StopAppControl();
730
750
  shutdown = DrainAndCloseAsync();
731
751
  shutdown.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
732
752
  }
@@ -753,6 +773,7 @@ namespace Arcane.Core.Hosts.Windows
753
773
  catch (Exception error) { ReportTaskFailure(coreLifetime, error); }
754
774
  }
755
775
  }
776
+ await DrainAppControlAsync();
756
777
  // Joining the independent image owner belongs after Core input has
757
778
  // closed; image I/O must never delay accepted service work draining.
758
779
  if (iconLoading != null)
@@ -781,6 +802,7 @@ namespace Arcane.Core.Hosts.Windows
781
802
  {
782
803
  webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
783
804
  if (injectedScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(injectedScript);
805
+ if (appControlScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(appControlScript);
784
806
  }
785
807
  }
786
808
  catch (Exception error) { Report(error); }
@@ -85,8 +85,11 @@ namespace Arcane.Core.Hosts.Windows
85
85
  ? Path.GetFullPath((string)stateRoot)
86
86
  : Path.Combine(defaultStateBase, "Arcane", appId);
87
87
  OpenDiagnostics(Path.Combine(profileParent, "Diagnostics"));
88
+ Dictionary<string, object> appControl = launchContext.ContainsKey("appControl")
89
+ ? RequiredRecord(launchContext, "appControl") : null;
88
90
  ArcaneHostOptions options = new ArcaneHostOptions
89
91
  {
92
+ ApplicationId = appId,
90
93
  ApplicationRoot = Path.GetFullPath(Path.Combine(directory, RequiredString(manifest, "webRoot"))),
91
94
  StartPath = RequiredString(manifest, "start"),
92
95
  OriginHost = "arcane.local",
@@ -97,6 +100,8 @@ namespace Arcane.Core.Hosts.Windows
97
100
  Resizable = OptionalWindowResizable(windowOptions),
98
101
  IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
99
102
  ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
103
+ AppControlEndpoint = appControl == null ? null : RequiredString(appControl, "endpoint"),
104
+ AppControlSource = appControl == null ? null : File.ReadAllText(Path.Combine(directory, "arcane-app-control.js")),
100
105
  CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
101
106
  // Forward the actual host selection separately; Core keeps
102
107
  // every explicit launch-record field unchanged.
@@ -0,0 +1,248 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.Threading;
5
+ using System.Threading.Tasks;
6
+ using Microsoft.Web.WebView2.Core;
7
+
8
+ namespace Arcane.Core.Hosts.Windows
9
+ {
10
+ // The form owns UI-thread dispatch and document lifetime. The pipe owner
11
+ // handles only connections and framed requests, never another Core child.
12
+ public sealed partial class ArcaneHostForm
13
+ {
14
+ private const string AppControlDocumentKey = "Symbol.for('arcane-os.app-control.document')";
15
+ private readonly HashSet<Task> appControlDocumentTasks = new HashSet<Task>();
16
+ private ArcaneAppControl appControl;
17
+ private Task appControlClosing;
18
+ private string appControlScript;
19
+ private string appControlDocument;
20
+ private long appControlGeneration;
21
+ private long appControlDocumentEpoch;
22
+ private ulong appControlNavigationId;
23
+ private bool appControlNavigating;
24
+
25
+ private void StartAppControl()
26
+ {
27
+ if (options.AppControlEndpoint == null) return;
28
+ if (String.IsNullOrEmpty(options.AppControlSource))
29
+ throw new ArgumentException("Supply the SDK-generated app-control source for the selected endpoint.");
30
+ appControl = new ArcaneAppControl(options.AppControlEndpoint, DispatchAppControl, DeliverDiagnostic, Report);
31
+ appControl.Completion.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
32
+ }
33
+
34
+ private async Task InstallAppControlAsync(CoreWebView2 browser)
35
+ {
36
+ if (appControl == null) return;
37
+ appControlScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(
38
+ "globalThis[" + AppControlDocumentKey + "] = globalThis.crypto.randomUUID();");
39
+ if (closing) return;
40
+ browser.NavigationStarting += AppControlNavigationStarting;
41
+ browser.NavigationCompleted += AppControlNavigationCompleted;
42
+ }
43
+
44
+ private void StopAppControl()
45
+ {
46
+ if (appControl == null || appControlClosing != null) return;
47
+ appControlClosing = appControl.CloseAsync();
48
+ }
49
+
50
+ private async Task DrainAppControlAsync()
51
+ {
52
+ // Ingress stops in BeginClose. Join only after the existing Core
53
+ // cancellation/close has started, with the UI pump still alive.
54
+ if (appControlClosing != null)
55
+ {
56
+ try { await appControlClosing; }
57
+ catch (Exception error) { ReportTaskFailure(appControlClosing, error); }
58
+ }
59
+ Task documents = Task.WhenAll(appControlDocumentTasks);
60
+ try { await documents; }
61
+ catch (Exception error) { ReportTaskFailure(documents, error); }
62
+ }
63
+
64
+ private void AppControlNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
65
+ {
66
+ appControlNavigationId = args.NavigationId;
67
+ appControlDocumentEpoch++;
68
+ appControlNavigating = true;
69
+ }
70
+
71
+ private void AppControlNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs args)
72
+ {
73
+ if (closing || browserProcessExited || args.NavigationId != appControlNavigationId) return;
74
+ // A cancelled navigation may leave the old document alive. Read its
75
+ // actual creation marker instead of treating every attempt as new.
76
+ Task task = RefreshAppControlDocumentAsync(args.NavigationId, appControlDocumentEpoch);
77
+ appControlDocumentTasks.Add(task);
78
+ task.ContinueWith(AppControlDocumentSettled, CancellationToken.None,
79
+ TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
80
+ }
81
+
82
+ private void AppControlRendererExited()
83
+ {
84
+ appControlDocumentEpoch++;
85
+ appControlDocument = null;
86
+ appControlNavigating = true;
87
+ }
88
+
89
+ private async Task RefreshAppControlDocumentAsync(ulong selectedNavigation, long selectedEpoch)
90
+ {
91
+ string json = await webView.CoreWebView2.ExecuteScriptAsync("globalThis[" + AppControlDocumentKey + "]");
92
+ if (closing || browserProcessExited || selectedNavigation != appControlNavigationId
93
+ || selectedEpoch != appControlDocumentEpoch) return;
94
+ string current = ArcaneHost.Serializer().DeserializeObject(json) as string;
95
+ if (current == null) throw new InvalidOperationException("The current document has no app-control lifecycle marker.");
96
+ if (current != appControlDocument)
97
+ {
98
+ appControlDocument = current;
99
+ appControlGeneration++;
100
+ }
101
+ appControlNavigating = false;
102
+ }
103
+
104
+ private void AppControlDocumentSettled(Task task)
105
+ {
106
+ appControlDocumentTasks.Remove(task);
107
+ if (task.IsFaulted) ReportTaskFailure(task, task.Exception);
108
+ }
109
+
110
+ private Task<object> DispatchAppControl(string method, Dictionary<string, object> parameters,
111
+ CancellationToken cancellation)
112
+ {
113
+ AppControlRequest request = new AppControlRequest(this, method, parameters, cancellation);
114
+ try
115
+ {
116
+ // The form's owned message pump stays alive through control and
117
+ // Core drain. No foreground window or desktop input is selected.
118
+ BeginInvoke(new Action(request.Run));
119
+ }
120
+ catch (Exception error) { request.Completion.TrySetException(error); }
121
+ return request.Completion.Task;
122
+ }
123
+
124
+ private bool AppControlDocumentReady()
125
+ {
126
+ return !closing && !browserProcessExited && !appControlNavigating
127
+ && appControlDocument != null && webView.CoreWebView2 != null;
128
+ }
129
+
130
+ private Dictionary<string, object> AppControlStatus()
131
+ {
132
+ return new Dictionary<string, object>
133
+ {
134
+ { "app", new Dictionary<string, object> { { "id", options.ApplicationId }, { "name", options.Title } } },
135
+ { "platform", "windows" }, { "host", "webview2" },
136
+ { "url", webView.Source == null ? null : webView.Source.AbsoluteUri },
137
+ { "documentGeneration", appControlGeneration }, { "ready", AppControlDocumentReady() },
138
+ { "window", new Dictionary<string, object>
139
+ { { "title", Text }, { "state", WindowState.ToString() },
140
+ { "width", ClientSize.Width }, { "height", ClientSize.Height } } },
141
+ { "operations", new string[] { "status", "inspect", "capture", "act" } }
142
+ };
143
+ }
144
+
145
+ private async Task<object> ExecuteAppControlAsync(string method, Dictionary<string, object> parameters,
146
+ CancellationToken cancellation)
147
+ {
148
+ cancellation.ThrowIfCancellationRequested();
149
+ if (method == "app.control.status") return AppControlStatus();
150
+ if (method != "app.control.inspect" && method != "app.control.capture" && method != "app.control.act")
151
+ throw new ArgumentException("Unknown app-control method: " + method);
152
+ if (!AppControlDocumentReady()) throw new InvalidOperationException("The app document is not ready for control.");
153
+ long selectedGeneration = appControlGeneration;
154
+ string selectedDocument = appControlDocument;
155
+ string selectedUrl = webView.Source == null ? null : webView.Source.AbsoluteUri;
156
+ object expected;
157
+ if (method == "app.control.act" && !parameters.ContainsKey("documentGeneration"))
158
+ throw new ArgumentException("An action requires the documentGeneration returned by status or inspect.");
159
+ if (parameters.TryGetValue("documentGeneration", out expected)
160
+ && (!(expected is int || expected is long || expected is decimal || expected is double)
161
+ || Convert.ToDouble(expected) != selectedGeneration))
162
+ throw new InvalidOperationException("The selected app document has been replaced.");
163
+
164
+ object result;
165
+ if (method == "app.control.capture")
166
+ {
167
+ using (MemoryStream image = new MemoryStream())
168
+ {
169
+ await webView.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Png, image);
170
+ result = new Dictionary<string, object>
171
+ {
172
+ { "mimeType", "image/png" }, { "data", Convert.ToBase64String(image.ToArray()) }
173
+ };
174
+ }
175
+ }
176
+ else
177
+ {
178
+ string operation = method == "app.control.inspect" ? "inspect" : "act";
179
+ // The creation marker is checked inside the executing document.
180
+ // Native generation checks alone cannot stop a queued script
181
+ // from reaching a successor document during navigation.
182
+ string script = "(function(expected,operation,parameters){"
183
+ + "if(globalThis[" + AppControlDocumentKey + "]!==expected)"
184
+ + "return {ok:false,error:{code:'APP_CONTROL_DOCUMENT_REPLACED',message:'The selected app document has been replaced.'}};"
185
+ + "return (" + options.AppControlSource + ")(operation,parameters);})("
186
+ + ArcaneHost.Serializer().Serialize(selectedDocument) + ","
187
+ + ArcaneHost.Serializer().Serialize(operation) + ","
188
+ + ArcaneHost.Serializer().Serialize(parameters) + ")";
189
+ string json = await webView.CoreWebView2.ExecuteScriptAsync(script);
190
+ Dictionary<string, object> envelope = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
191
+ object ok;
192
+ if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool))
193
+ throw new InvalidDataException("The document returned no app-control result.");
194
+ if (!(bool)ok)
195
+ {
196
+ object detail;
197
+ envelope.TryGetValue("error", out detail);
198
+ IOException error = new IOException("The document app-control operation failed.");
199
+ error.Data["documentError"] = detail;
200
+ throw error;
201
+ }
202
+ if (!envelope.TryGetValue("result", out result))
203
+ throw new InvalidDataException("The document omitted the app-control result.");
204
+ }
205
+ // Completion is observed after dispatch even if a caller cancelled.
206
+ // A document change is reported with the actual returned result; it
207
+ // neither retries an action nor claims its effects were reversed.
208
+ if (!AppControlDocumentReady() || appControlGeneration != selectedGeneration || appControlDocument != selectedDocument)
209
+ {
210
+ IOException error = new IOException("The app document changed while the operation was completing.");
211
+ error.Data["result"] = result;
212
+ error.Data["documentGeneration"] = selectedGeneration;
213
+ error.Data["url"] = selectedUrl;
214
+ throw error;
215
+ }
216
+ Dictionary<string, object> record = result as Dictionary<string, object>;
217
+ if (record == null) throw new InvalidDataException("The app-control result must be an object.");
218
+ record["documentGeneration"] = selectedGeneration;
219
+ record["url"] = webView.Source == null ? null : webView.Source.AbsoluteUri;
220
+ return record;
221
+ }
222
+
223
+ private sealed class AppControlRequest
224
+ {
225
+ private readonly ArcaneHostForm window;
226
+ private readonly string method;
227
+ private readonly Dictionary<string, object> parameters;
228
+ private readonly CancellationToken cancellation;
229
+ internal readonly TaskCompletionSource<object> Completion = new TaskCompletionSource<object>();
230
+
231
+ internal AppControlRequest(ArcaneHostForm window, string method,
232
+ Dictionary<string, object> parameters, CancellationToken cancellation)
233
+ {
234
+ this.window = window;
235
+ this.method = method;
236
+ this.parameters = parameters;
237
+ this.cancellation = cancellation;
238
+ }
239
+
240
+ internal async void Run()
241
+ {
242
+ try { Completion.TrySetResult(await window.ExecuteAppControlAsync(method, parameters, cancellation)); }
243
+ catch (OperationCanceledException) { Completion.TrySetCanceled(); }
244
+ catch (Exception error) { Completion.TrySetException(error); }
245
+ }
246
+ }
247
+ }
248
+ }
@@ -1,11 +1,11 @@
1
1
  import {spawn} from 'node:child_process';
2
2
  import {open} from 'node:fs/promises';
3
- import {createConnection, createServer} from 'node:net';
3
+ import {createServer} from 'node:net';
4
4
  import {randomUUID} from 'node:crypto';
5
- import {createCoreClient} from '../../browser-runtime/core/client.mjs';
6
5
  import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
7
6
  import {createCoreRuntime} from './runtime.mjs';
8
7
  import {createCoreFrameDecoder, encodeCoreFrame} from './stdio.mjs';
8
+ import {connectCoreSocket, createCoreSocketConnection, writeCoreSocket as write} from './socket-connection.mjs';
9
9
 
10
10
  function failure(code, message) { return new CoreError({code, message}); }
11
11
  function reportError(error) { console.error('Shared Arcane Core failed:', error); }
@@ -23,29 +23,6 @@ function response(id, result, error) {
23
23
  time: new Date().toISOString()};
24
24
  }
25
25
 
26
- // A stream write owns terminal events as well as its callback. Some destroyed
27
- // streams never deliver that callback; retirement must still settle the write.
28
- function write(output, content) {
29
- return new Promise(function writeFrame(resolve, reject) {
30
- let settled = false;
31
- function finish(error) {
32
- if (settled) return;
33
- settled = true;
34
- output.off('error', finish);
35
- output.off('close', closed);
36
- output.off('finish', closed);
37
- if (error) reject(error);
38
- else resolve();
39
- }
40
- function closed() { finish(failure('CORE_CONNECTION_CLOSED', 'The Core output closed during a write.')); }
41
- output.once('error', finish);
42
- output.once('close', closed);
43
- output.once('finish', closed);
44
- if (output.destroyed || output.writableEnded) { closed(); return; }
45
- try { output.write(content, finish); } catch (error) { finish(error); }
46
- });
47
- }
48
-
49
26
  /** Claim one app-selected pipe/socket before importing or creating services. */
50
27
  export async function startSharedCoreHost({
51
28
  endpoint, application, version, configure, getReplayEvents, signal, onError = reportError
@@ -268,29 +245,7 @@ export async function startSharedCoreHost({
268
245
  }
269
246
 
270
247
  function connectSocket(endpoint, signal) {
271
- signal?.throwIfAborted();
272
- if (typeof endpoint !== 'string' || !endpoint) throw new TypeError('A shared Core endpoint must be a nonempty local pipe/socket path.');
273
- return new Promise(function connect(resolve, reject) {
274
- const socket = createConnection({path: endpoint});
275
- socket.pause();
276
- function cleanup() {
277
- signal?.removeEventListener('abort', abort);
278
- socket.off('connect', connected);
279
- socket.off('error', failed);
280
- }
281
- function failed(error) {
282
- cleanup();
283
- socket.on('error', reportError);
284
- socket.destroy();
285
- reject(error);
286
- }
287
- function abort() { failed(signal.reason); }
288
- function connected() { cleanup(); resolve(socket); }
289
- socket.once('connect', connected);
290
- socket.once('error', failed);
291
- signal?.addEventListener('abort', abort, {once: true});
292
- if (signal?.aborted) abort();
293
- });
248
+ return connectCoreSocket(endpoint, signal, reportError);
294
249
  }
295
250
 
296
251
  // The child retains its own listener and complete diagnostic file after the
@@ -354,49 +309,10 @@ function launchSelection(start) {
354
309
  /** Connect/reuse, optionally starting an explicit independent headless entry. */
355
310
  export async function connectSharedCoreHost({endpoint, start, signal, onError = reportError} = {}) {
356
311
  const socket = await discover({endpoint, start: launchSelection(start), signal});
357
- let client;
358
- let receive;
359
- let closing = false;
360
- let writes = Promise.resolve();
361
- let resolveClosed;
362
- let rejectClosed;
363
- const closed = new Promise(function ownConnection(resolve, reject) { resolveClosed = resolve; rejectClosed = reject; });
364
- closed.catch(function observeConnectionFailure() {});
365
- function diagnostic(error) { report(onError, error); }
366
- function failed(error) { client?.failTransport(error); socket.destroy(); rejectClosed(error); }
367
- const decoder = createCoreFrameDecoder(function frame(value) { receive(value); });
368
- socket.on('data', function data(chunk) { try { decoder.push(chunk); } catch (error) { failed(error); } });
369
- socket.on('error', failed);
370
- socket.on('end', function ended() {
371
- try { decoder.finish(); } catch (error) { failed(error); }
372
- });
373
- socket.on('close', function disconnected() {
374
- signal?.removeEventListener('abort', abort);
375
- if (!closing) client?.failTransport(failure('CORE_CONNECTION_CLOSED', 'The shared Core connection closed.'));
376
- resolveClosed();
377
- });
378
- const transport = {
379
- name: 'shared-core',
380
- subscribe(listener) {
381
- receive = listener;
382
- return function disconnect() {
383
- closing = true;
384
- writes.finally(function retireSocket() { socket.end(); }).catch(diagnostic);
385
- };
386
- },
387
- send(frame) {
388
- const content = encodeCoreFrame(frame);
389
- writes = writes.then(function sendFrame() { return write(socket, content); });
390
- writes.catch(failed);
391
- return writes;
392
- }
393
- };
394
- client = createCoreClient({transport, replayRuntimeState: true, onError: diagnostic});
395
- function abort() { client.close(); }
396
- signal?.addEventListener('abort', abort, {once: true});
397
- if (signal?.aborted) abort();
398
- socket.resume();
399
- return {client, endpoint, closed, close: function close() { client.close(); return closed; },
312
+ const connection = createCoreSocketConnection({socket, endpoint, signal, onError,
313
+ replayRuntimeState: true, name: 'shared-core', closedMessage: 'The shared Core connection closed.'});
314
+ const {client, closed} = connection;
315
+ return {...connection,
400
316
  async shutdown(options) {
401
317
  const result = await client.invoke('core.host.shutdown', {}, options);
402
318
  client.close();