opentray 0.14.4 → 0.16.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/dist/index.mjs CHANGED
@@ -1,12 +1,126 @@
1
1
  import { createRequire } from "node:module";
2
- import { PROTOCOL_VERSION, PROTOCOL_VERSION as PROTOCOL_VERSION$1, createBrokerEndpointIdentity, createBrokerEndpointIdentity as createBrokerEndpointIdentity$1, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatUnixSocketPath as formatUnixSocketPath$1, formatWindowsPipeName, formatWindowsPipeName as formatWindowsPipeName$1, isSupportedProtocolVersion, parseServerFrame, sanitizeCallerLabel } from "@opentray/spec";
2
+ import { PROTOCOL_VERSION, PROTOCOL_VERSION as PROTOCOL_VERSION$1, brokerArtifactIdentityEquals, createBrokerEndpointIdentity, createBrokerEndpointIdentity as createBrokerEndpointIdentity$1, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatUnixSocketPath as formatUnixSocketPath$1, formatWindowsPipeName, formatWindowsPipeName as formatWindowsPipeName$1, isBrokerArtifactIdentity, isSupportedProtocolVersion, parseServerFrame, sanitizeCallerLabel } from "@opentray/spec";
3
+ import { access, mkdir, open, readFile, realpath, rm, unlink, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join } from "node:path";
3
5
  import { createConnection } from "node:net";
4
6
  import { homedir } from "node:os";
5
7
  import { fileURLToPath } from "node:url";
6
- import { basename, dirname, join } from "node:path";
7
8
  import { constants } from "node:fs";
8
- import { access, mkdir, open, readFile, rm, unlink, writeFile } from "node:fs/promises";
9
9
  import { spawn } from "node:child_process";
10
+ import { createHash } from "node:crypto";
11
+ //#region src/native-extension-artifact.ts
12
+ var NativeExtensionArtifactResolutionError = class extends Error {
13
+ target;
14
+ code = "OPENTRAY_NATIVE_EXTENSION_ARTIFACT_RESOLUTION_FAILED";
15
+ packageName;
16
+ facadePackageJsonUrl;
17
+ constructor(message, target, options = {}) {
18
+ super(message, options);
19
+ this.target = target;
20
+ this.name = "NativeExtensionArtifactResolutionError";
21
+ if (options.packageName !== void 0) this.packageName = options.packageName;
22
+ if (options.facadePackageJsonUrl !== void 0) this.facadePackageJsonUrl = options.facadePackageJsonUrl;
23
+ }
24
+ };
25
+ /** Resolve one exact native library from the dependency closure that owns the facade. */
26
+ const resolveNativeExtensionArtifact = async (artifact, platform = process.platform, arch = process.arch) => {
27
+ const target = nativeExtensionTarget(platform, arch);
28
+ if (artifact.kind === "file") return {
29
+ path: await resolveAccessibleLibrary(artifact.path, target),
30
+ expectedIdentity: artifact.expectedIdentity ?? await resolveExpectedIdentity(artifact.identitySource, target),
31
+ target
32
+ };
33
+ const packageTarget = artifact.targets[target];
34
+ if (packageTarget === void 0) throw new NativeExtensionArtifactResolutionError(`native extension does not support target ${target}`, target);
35
+ const expectedIdentity = await resolveExpectedIdentity(artifact, target);
36
+ const resolveFromFacade = createRequire(artifact.packageJsonUrl);
37
+ let platformPackageJsonPath;
38
+ try {
39
+ platformPackageJsonPath = resolveFromFacade.resolve(`${packageTarget.packageName}/package.json`);
40
+ } catch (cause) {
41
+ throw new NativeExtensionArtifactResolutionError(`unable to resolve native extension package "${packageTarget.packageName}" for ${target} from ${artifact.packageJsonUrl}`, target, {
42
+ cause,
43
+ packageName: packageTarget.packageName,
44
+ facadePackageJsonUrl: artifact.packageJsonUrl
45
+ });
46
+ }
47
+ const manifest = await readPlatformPackageManifest(platformPackageJsonPath, target, packageTarget.packageName);
48
+ return {
49
+ path: await resolveAccessibleLibrary(join(dirname(platformPackageJsonPath), packageTarget.libraryPath), target, packageTarget.packageName),
50
+ packageName: packageTarget.packageName,
51
+ packageVersion: manifest.version,
52
+ expectedIdentity,
53
+ target
54
+ };
55
+ };
56
+ const resolveExpectedIdentity = async (source, target) => {
57
+ const [facadeManifest, contractManifest] = await Promise.all([readFacadePackageManifest(source.packageJsonUrl, target), readExtensionContractManifest(source.contractManifestUrl, target)]);
58
+ const [os, arch] = splitTarget(target);
59
+ return {
60
+ extensionName: contractManifest.extensionName,
61
+ artifactSetVersion: facadeManifest.version,
62
+ contractFingerprint: contractManifest.contractFingerprint,
63
+ target: {
64
+ os,
65
+ arch
66
+ }
67
+ };
68
+ };
69
+ const nativeExtensionTarget = (platform, arch) => {
70
+ if (arch !== "arm64" && arch !== "x64") throw new Error(`unsupported native extension architecture: ${arch}`);
71
+ return `${platform}-${arch}`;
72
+ };
73
+ const readFacadePackageManifest = async (packageJsonUrl, target) => {
74
+ const parsed = await readJsonUrl(packageJsonUrl, target, "facade package manifest");
75
+ if (!isFacadePackageManifest(parsed)) throw new NativeExtensionArtifactResolutionError(`native extension facade manifest is invalid at ${packageJsonUrl}`, target, { facadePackageJsonUrl: packageJsonUrl });
76
+ return parsed;
77
+ };
78
+ const readExtensionContractManifest = async (contractManifestUrl, target) => {
79
+ const parsed = await readJsonUrl(contractManifestUrl, target, "extension contract manifest");
80
+ if (!isExtensionContractManifest(parsed)) throw new NativeExtensionArtifactResolutionError(`native extension contract manifest is invalid at ${contractManifestUrl}`, target);
81
+ return parsed;
82
+ };
83
+ const readJsonUrl = async (url, target, label) => {
84
+ try {
85
+ return JSON.parse(await readFile(new URL(url), "utf8"));
86
+ } catch (cause) {
87
+ throw new NativeExtensionArtifactResolutionError(`unable to read ${label} at ${url}`, target, { cause });
88
+ }
89
+ };
90
+ const readPlatformPackageManifest = async (packageJsonPath, target, packageName) => {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
94
+ } catch (cause) {
95
+ throw new NativeExtensionArtifactResolutionError(`unable to read native extension package manifest at ${packageJsonPath}`, target, {
96
+ cause,
97
+ packageName
98
+ });
99
+ }
100
+ if (!isPlatformPackageManifest(parsed) || parsed.name !== packageName) throw new NativeExtensionArtifactResolutionError(`native extension package manifest at ${packageJsonPath} does not identify ${packageName}`, target, { packageName });
101
+ const [platform, arch] = splitTarget(target);
102
+ if (parsed.os !== void 0 && !parsed.os.includes(platform)) throw new NativeExtensionArtifactResolutionError(`native extension package ${packageName} does not support os ${platform}`, target, { packageName });
103
+ if (parsed.cpu !== void 0 && !parsed.cpu.includes(arch)) throw new NativeExtensionArtifactResolutionError(`native extension package ${packageName} does not support cpu ${arch}`, target, { packageName });
104
+ return parsed;
105
+ };
106
+ const isPlatformPackageManifest = (value) => typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" && "version" in value && typeof value.version === "string" && (!("os" in value) || Array.isArray(value.os) && value.os.every((item) => typeof item === "string")) && (!("cpu" in value) || Array.isArray(value.cpu) && value.cpu.every((item) => typeof item === "string"));
107
+ const isFacadePackageManifest = (value) => typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" && "version" in value && typeof value.version === "string";
108
+ const isExtensionContractManifest = (value) => typeof value === "object" && value !== null && "extensionName" in value && typeof value.extensionName === "string" && "contractFingerprint" in value && typeof value.contractFingerprint === "string";
109
+ const splitTarget = (target) => {
110
+ const separator = target.lastIndexOf("-");
111
+ return [target.slice(0, separator), target.slice(separator + 1)];
112
+ };
113
+ const resolveAccessibleLibrary = async (path, target, packageName) => {
114
+ try {
115
+ return await realpath(path);
116
+ } catch (cause) {
117
+ throw new NativeExtensionArtifactResolutionError(`native extension library is not accessible at ${path}`, target, {
118
+ cause,
119
+ ...packageName === void 0 ? {} : { packageName }
120
+ });
121
+ }
122
+ };
123
+ //#endregion
10
124
  //#region src/client.ts
11
125
  const createInitFrame = (clientVersion) => ({
12
126
  type: "init",
@@ -15,8 +129,27 @@ const createInitFrame = (clientVersion) => ({
15
129
  });
16
130
  function createClient(transport, options = {}) {
17
131
  const nextRequestId = createRequestIdFactory(options.requestIdPrefix ?? "opentray");
132
+ let appPromise;
133
+ const ensureApp = async (trayOptions) => {
134
+ appPromise ??= (async () => {
135
+ const app = await resolveDefaultAppRef(transport, nextRequestId);
136
+ const handle = createAppHandle(transport, app.appId, nextRequestId);
137
+ const configured = options.appOptions;
138
+ if (configured?.name !== void 0) await handle.setName(configured.name);
139
+ if (configured?.icon !== void 0) await handle.setIcon(configured.icon);
140
+ else if (trayOptions.icon !== void 0 && await handle.getIcon() === null) await handle.setIcon(trayOptions.icon);
141
+ return {
142
+ app,
143
+ handle
144
+ };
145
+ })().catch((error) => {
146
+ appPromise = void 0;
147
+ throw error;
148
+ });
149
+ return appPromise;
150
+ };
18
151
  return { async createTray(options) {
19
- const app = await resolveDefaultAppRef(transport, nextRequestId);
152
+ const { app } = await ensureApp(options);
20
153
  const requestId = nextRequestId();
21
154
  const frame = expectResponse(await transport.request({
22
155
  type: "create-tray",
@@ -36,6 +169,7 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
36
169
  };
37
170
  const handle = {
38
171
  trayId,
172
+ app: createAppHandle(transport, appId, nextRequestId),
39
173
  async getBounds() {
40
174
  const requestId = nextRequestId();
41
175
  return expectResponse(await transport.request({
@@ -77,12 +211,14 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
77
211
  },
78
212
  async loadExtension(options) {
79
213
  const requestId = nextRequestId();
214
+ const resolved = await resolveNativeExtensionArtifact(options.artifact);
80
215
  expectResponse(await transport.request({
81
216
  type: "load-ext",
82
217
  requestId,
83
218
  appId,
84
219
  name: options.name,
85
- path: options.path,
220
+ path: resolved.path,
221
+ expectedIdentity: resolved.expectedIdentity,
86
222
  ...options.mountId === void 0 ? {} : { mountId: options.mountId }
87
223
  }), requestId, "ack");
88
224
  },
@@ -172,16 +308,55 @@ const resolveDefaultAppRef = async (transport, nextRequestId) => {
172
308
  requestId
173
309
  }), requestId, "default-app").app;
174
310
  };
311
+ const createAppHandle = (transport, appId, nextRequestId) => {
312
+ const requestIdentity = async () => {
313
+ const requestId = nextRequestId();
314
+ return expectResponse(await transport.request({
315
+ type: "get-app-identity",
316
+ requestId,
317
+ appId
318
+ }), requestId, "app-identity").identity;
319
+ };
320
+ return {
321
+ appId,
322
+ async getName() {
323
+ return (await requestIdentity()).appName;
324
+ },
325
+ async setName(name) {
326
+ const requestId = nextRequestId();
327
+ expectResponse(await transport.request({
328
+ type: "set-app-name",
329
+ requestId,
330
+ appId,
331
+ name
332
+ }), requestId, "ack");
333
+ return (await requestIdentity()).appName;
334
+ },
335
+ async getIcon() {
336
+ return (await requestIdentity()).icon ?? null;
337
+ },
338
+ async setIcon(icon) {
339
+ const requestId = nextRequestId();
340
+ expectResponse(await transport.request({
341
+ type: "set-app-icon",
342
+ requestId,
343
+ appId,
344
+ icon
345
+ }), requestId, "ack");
346
+ return icon;
347
+ }
348
+ };
349
+ };
175
350
  const createTrayExtensionContext = (tray, appId, extension, options, nextMountId) => {
176
351
  const mount = extension.resolveMount?.(options) ?? {};
177
352
  const name = mount.name ?? extension.name;
178
- const path = mount.path ?? extension.path;
353
+ const artifact = mount.artifact ?? extension.artifact;
179
354
  const mountId = mount.mountId ?? nextMountId(name);
180
355
  let loadPromise;
181
356
  const ensureLoaded = async () => {
182
357
  if (loadPromise === void 0) loadPromise = tray.loadExtension({
183
358
  name,
184
- path,
359
+ artifact,
185
360
  mountId
186
361
  }).catch((error) => {
187
362
  loadPromise = void 0;
@@ -193,7 +368,7 @@ const createTrayExtensionContext = (tray, appId, extension, options, nextMountId
193
368
  appId,
194
369
  trayId: tray.trayId,
195
370
  name,
196
- path,
371
+ artifact,
197
372
  mountId,
198
373
  ensureLoaded,
199
374
  async command(data) {
@@ -228,6 +403,7 @@ const requestIdOf = (frame) => {
228
403
  switch (frame.type) {
229
404
  case "app-created":
230
405
  case "default-app":
406
+ case "app-identity":
231
407
  case "tray-created":
232
408
  case "tray-bounds":
233
409
  case "ack":
@@ -271,6 +447,13 @@ const resolveBrokerNativeTarget = (platform = process.platform, arch = process.a
271
447
  binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`
272
448
  };
273
449
  };
450
+ const resolveBrokerArtifactTarget = (platform = process.platform, arch = process.arch) => {
451
+ const packageOs = platformToPackageOs(platform);
452
+ return {
453
+ os: packageOs === "windows" ? "win32" : packageOs,
454
+ arch: normalizeArch(arch)
455
+ };
456
+ };
274
457
  const platformToPackageOs = (platform) => {
275
458
  switch (platform) {
276
459
  case "darwin":
@@ -305,6 +488,31 @@ var MissingPlatformBrokerBinaryError = class extends Error {
305
488
  //#region src/daemon/broker-command.ts
306
489
  const sourceUrl = import.meta.url;
307
490
  const requireFromSource = createRequire(sourceUrl);
491
+ const resolveBrokerArtifact = async (paths, options = {}) => {
492
+ const command = await resolveBrokerCommand(paths, options);
493
+ const executablePath = await (options.resolveExecutablePath ?? realpath)(command.command);
494
+ const bytes = await (options.readExecutable ?? readFile)(executablePath);
495
+ const executableHash = createHash("sha256").update(bytes).digest("hex");
496
+ const artifactIdentity = {
497
+ packageVersion: paths.packageVersion,
498
+ target: resolveBrokerArtifactTarget(options.platform ?? process.platform, options.arch ?? process.arch),
499
+ executableHash,
500
+ buildIdentity: `sha256:${executableHash.slice(0, 16)}`
501
+ };
502
+ return {
503
+ ...command,
504
+ command: executablePath,
505
+ args: [
506
+ ...command.args,
507
+ "--broker-executable-path",
508
+ executablePath,
509
+ "--broker-artifact-identity",
510
+ JSON.stringify(artifactIdentity)
511
+ ],
512
+ executablePath,
513
+ artifactIdentity
514
+ };
515
+ };
308
516
  const resolveBrokerCommand = async (paths, options = {}) => {
309
517
  const explicit = (options.env ?? process.env).OPENTRAY_BROKER_BIN;
310
518
  if (explicit !== void 0 && explicit.length > 0) return commandForBinary(explicit, paths);
@@ -315,7 +523,7 @@ const resolveBrokerCommand = async (paths, options = {}) => {
315
523
  if (installed.binary !== void 0) return commandForBinary(installed.binary, paths);
316
524
  const sourceDir = options.sourceDir ?? dirname(fileURLToPath(sourceUrl));
317
525
  const workspaceRoot = await (options.findWorkspaceRoot ?? findWorkspaceRoot)(sourceDir);
318
- if (workspaceRoot !== void 0) return commandForBinary(await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot), paths);
526
+ if (workspaceRoot !== void 0) return commandForBinary(await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot, paths, platform), paths);
319
527
  throw new MissingPlatformBrokerBinaryError(installed.binaryPath === void 0 ? `unable to resolve OpenTray broker binary for ${platform}/${arch}; install "${target.packageName}" for this platform or set OPENTRAY_BROKER_BIN` : `unable to resolve OpenTray broker binary for ${platform}/${arch}; package "${target.packageName}" was found but binary "${installed.binaryPath}" is not accessible; restage the runtime package or set OPENTRAY_BROKER_BIN`, platform, arch, {
320
528
  packageName: target.packageName,
321
529
  ...installed.binaryPath === void 0 ? {} : { binaryPath: installed.binaryPath }
@@ -377,22 +585,20 @@ const findWorkspaceRoot = async (start) => {
377
585
  current = parent;
378
586
  }
379
587
  };
380
- const resolveDevBrokerBinaryPath = (workspaceRoot, platform = process.platform) => join(workspaceRoot, "target", "debug", platform === "win32" ? "opentray.exe" : "opentray");
381
- const terminateWorkspaceDevBrokerProcess = async (workspaceRoot, platform = process.platform) => {
382
- if (platform !== "win32") return;
383
- await terminateWindowsProcessByExecutable(resolveDevBrokerBinaryPath(workspaceRoot, platform));
384
- };
385
- const ensureDevBrokerBinary = async (workspaceRoot) => {
386
- const binary = resolveDevBrokerBinaryPath(workspaceRoot);
387
- await terminateWorkspaceDevBrokerProcess(workspaceRoot);
388
- await runCargoBuild(workspaceRoot);
588
+ const resolveDevBrokerBinaryPath = (workspaceRoot, platform = process.platform, callerLabel = "opentray") => join(resolveDevBrokerTargetDir(workspaceRoot, platform, callerLabel), "debug", platform === "win32" ? "opentray.exe" : "opentray");
589
+ const resolveDevBrokerTargetDir = (workspaceRoot, platform, callerLabel) => platform === "win32" ? join(workspaceRoot, "target", "opentray-source", callerLabel) : join(workspaceRoot, "target");
590
+ const ensureDevBrokerBinary = async (workspaceRoot, paths, platform) => {
591
+ const targetDir = resolveDevBrokerTargetDir(workspaceRoot, platform, paths.callerLabel);
592
+ const binary = resolveDevBrokerBinaryPath(workspaceRoot, platform, paths.callerLabel);
593
+ await runCargoBuild(workspaceRoot, platform === "win32" ? targetDir : void 0);
389
594
  return binary;
390
595
  };
391
- const runCargoBuild = (workspaceRoot) => new Promise((resolve, reject) => {
596
+ const runCargoBuild = (workspaceRoot, targetDir) => new Promise((resolve, reject) => {
392
597
  const child = spawn("cargo", [
393
598
  "build",
394
599
  "-p",
395
- "opentray-bin"
600
+ "opentray-bin",
601
+ ...targetDir === void 0 ? [] : ["--target-dir", targetDir]
396
602
  ], {
397
603
  cwd: workspaceRoot,
398
604
  stdio: process.env.OPENTRAY_BROKER_BUILD_LOGS === "1" ? "inherit" : "ignore"
@@ -414,61 +620,25 @@ const exists = async (path) => {
414
620
  return false;
415
621
  }
416
622
  };
417
- const terminateWindowsProcessByExecutable = (binary) => new Promise((resolve, reject) => {
418
- const child = spawn("powershell.exe", [
419
- "-NoProfile",
420
- "-NonInteractive",
421
- "-ExecutionPolicy",
422
- "Bypass",
423
- "-Command",
424
- String.raw`
425
- $ErrorActionPreference = 'Stop'
426
- $target = [System.IO.Path]::GetFullPath(${powerShellString(binary)})
427
- $processes = @(Get-CimInstance Win32_Process -Filter "Name = 'opentray.exe'" | Where-Object {
428
- $_.ExecutablePath -and ([System.IO.Path]::GetFullPath($_.ExecutablePath) -ieq $target)
429
- })
430
- foreach ($process in $processes) {
431
- Stop-Process -Id $process.ProcessId -Force -ErrorAction Stop
432
- }
433
- foreach ($process in $processes) {
434
- Wait-Process -Id $process.ProcessId -Timeout 5 -ErrorAction SilentlyContinue
435
- }
436
- `
437
- ], { stdio: [
438
- "ignore",
439
- "ignore",
440
- "pipe"
441
- ] });
442
- let stderr = "";
443
- child.stderr?.on("data", (chunk) => {
444
- stderr += String(chunk);
445
- });
446
- child.once("error", reject);
447
- child.once("exit", (code) => {
448
- if (code === 0) {
449
- resolve();
450
- return;
451
- }
452
- reject(/* @__PURE__ */ new Error(`failed to stop running dev broker before cargo build: ${stderr.trim() || code}`));
453
- });
454
- });
455
623
  const binaryAccessMode = (platform) => platform === "win32" ? constants.F_OK : constants.X_OK;
456
624
  const assertInstalledBrokerBinaryAccessible = async (binaryPath, platform) => {
457
625
  await access(binaryPath, binaryAccessMode(platform));
458
626
  };
459
- const powerShellString = (value) => `'${value.replace(/'/g, "''")}'`;
460
627
  function isNodeError$1(error) {
461
628
  return error instanceof Error && "code" in error;
462
629
  }
463
630
  //#endregion
464
631
  //#region src/daemon/lifecycle.ts
465
632
  const DAEMON_STDIO_ENV = "OPENTRAY_DAEMON_STDIO";
633
+ const DEFAULT_DAEMON_LOCK_TIMEOUT_MS = 5e3;
466
634
  const createNodeDaemonDriver = (cliEntrypoint) => ({
635
+ async resolveBroker(paths) {
636
+ return resolveBrokerArtifact(paths);
637
+ },
467
638
  async isAlive(pid) {
468
639
  return isProcessAlive(pid);
469
640
  },
470
- async spawnBroker(paths) {
471
- const broker = await resolveBrokerCommand(paths);
641
+ async spawnBroker(paths, broker) {
472
642
  const child = spawn(broker.command, broker.args, {
473
643
  cwd: broker.cwd,
474
644
  detached: true,
@@ -493,25 +663,41 @@ const createNodeDaemonDriver = (cliEntrypoint) => ({
493
663
  const resolveBrokerStdio = (value) => {
494
664
  return value === "inherit" ? "inherit" : "ignore";
495
665
  };
496
- const startDaemon = async ({ paths, driver, lockTimeoutMs = 1e3 }) => {
666
+ const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS }) => {
667
+ const broker = await driver.resolveBroker(paths);
497
668
  await mkdir(paths.runtimeDir, { recursive: true });
498
669
  const lock = await acquireLock(paths.lockFile, lockTimeoutMs);
499
670
  try {
500
671
  const existingPid = await readPid(paths.pidFile);
501
- if (existingPid !== void 0 && await driver.isAlive(existingPid)) return {
502
- status: "already-running",
503
- pid: existingPid,
504
- paths
505
- };
672
+ if (existingPid !== void 0 && await driver.isAlive(existingPid)) {
673
+ if (readyMatchesBroker(await readReadyMetadata(paths.readyFile), paths, existingPid, broker)) return {
674
+ status: "already-running",
675
+ pid: existingPid,
676
+ paths,
677
+ broker
678
+ };
679
+ await driver.stop(existingPid);
680
+ if (!await waitUntilStopped(driver, existingPid)) throw new Error(`incompatible daemon broker did not stop within the bounded shutdown window: pid=${existingPid}`);
681
+ }
506
682
  await cleanupRuntimeFiles(paths);
507
- const pid = await driver.spawnBroker(paths);
683
+ const pid = await driver.spawnBroker(paths, broker);
508
684
  if (!Number.isInteger(pid) || pid <= 0) throw new Error("daemon broker process did not expose a valid pid");
509
685
  await writeFile(paths.pidFile, `${pid}\n`, "utf8");
510
- await waitForReadyFile(paths.readyFile, driver, pid);
686
+ try {
687
+ await waitForReadyFile(paths, driver, pid, broker);
688
+ } catch (error) {
689
+ if (await driver.isAlive(pid)) {
690
+ await driver.stop(pid);
691
+ await waitUntilStopped(driver, pid);
692
+ }
693
+ await cleanupRuntimeFiles(paths);
694
+ throw error;
695
+ }
511
696
  return {
512
697
  status: "started",
513
698
  pid,
514
- paths
699
+ paths,
700
+ broker
515
701
  };
516
702
  } finally {
517
703
  await lock.release();
@@ -532,16 +718,50 @@ const cleanupRuntimeFiles = async (paths) => {
532
718
  await rm(paths.pidFile, { force: true });
533
719
  if (process.platform !== "win32") await rm(paths.endpoint, { force: true });
534
720
  };
535
- const waitForReadyFile = async (readyFile, driver, pid) => {
536
- for (let attempt = 0; attempt < 40; attempt += 1) try {
537
- await readFile(readyFile, "utf8");
538
- return;
539
- } catch (error) {
540
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
541
- if (!await driver.isAlive(pid)) throw new Error(`daemon broker exited before readiness: pid=${pid}; readyFile=${readyFile}; set ${DAEMON_STDIO_ENV}=inherit to inspect broker stderr`);
721
+ const waitUntilStopped = async (driver, pid) => {
722
+ for (let attempt = 0; attempt < 20; attempt += 1) {
723
+ if (!await driver.isAlive(pid)) return true;
724
+ await sleep(50);
725
+ }
726
+ return false;
727
+ };
728
+ const waitForReadyFile = async (paths, driver, pid, broker) => {
729
+ for (let attempt = 0; attempt < 40; attempt += 1) {
730
+ const ready = await readReadyMetadata(paths.readyFile);
731
+ if (readyMatchesBroker(ready, paths, pid, broker)) return;
732
+ if (ready !== void 0) throw new Error(`daemon readiness artifact identity mismatch: expected=${JSON.stringify(broker.artifactIdentity)} actual=${JSON.stringify(ready.brokerArtifactIdentity)}`);
733
+ if (!await driver.isAlive(pid)) throw new Error(`daemon broker exited before readiness: pid=${pid}; readyFile=${paths.readyFile}; set ${DAEMON_STDIO_ENV}=inherit to inspect broker stderr`);
542
734
  await sleep(50);
543
735
  }
544
- throw new Error(`timed out waiting for daemon readiness: ${readyFile}`);
736
+ throw new Error(`timed out waiting for daemon readiness: ${paths.readyFile}`);
737
+ };
738
+ const readReadyMetadata = async (readyFile) => {
739
+ let value;
740
+ try {
741
+ value = JSON.parse(await readFile(readyFile, "utf8"));
742
+ } catch (error) {
743
+ if (isNodeError(error) && error.code === "ENOENT") return;
744
+ if (error instanceof SyntaxError) return;
745
+ throw error;
746
+ }
747
+ if (!isRecord(value) || !Number.isInteger(value.pid) || typeof value.pid !== "number" || typeof value.endpoint !== "string" || typeof value.packageVersion !== "string" || typeof value.protocolVersion !== "number" || typeof value.appId !== "string" || typeof value.appName !== "string" || typeof value.callerLabel !== "string" || typeof value.executablePath !== "string" || !isBrokerArtifactIdentity(value.brokerArtifactIdentity)) return;
748
+ return {
749
+ pid: value.pid,
750
+ endpoint: value.endpoint,
751
+ packageVersion: value.packageVersion,
752
+ protocolVersion: value.protocolVersion,
753
+ appId: value.appId,
754
+ appName: value.appName,
755
+ callerLabel: value.callerLabel,
756
+ executablePath: value.executablePath,
757
+ brokerArtifactIdentity: value.brokerArtifactIdentity
758
+ };
759
+ };
760
+ const readyMatchesBroker = (ready, paths, pid, broker) => ready !== void 0 && ready.pid === pid && ready.endpoint === paths.endpoint && ready.packageVersion === paths.packageVersion && ready.protocolVersion === paths.protocolVersion && ready.appId === paths.appId && ready.appName === paths.appName && ready.callerLabel === paths.callerLabel && executablePathsEqual(ready.executablePath, broker.executablePath) && brokerArtifactIdentityEquals(ready.brokerArtifactIdentity, broker.artifactIdentity);
761
+ const executablePathsEqual = (left, right, platform = process.platform) => {
762
+ if (platform !== "win32") return left === right;
763
+ const normalize = (value) => value.replaceAll("\\", "/").replace(/^\/\/?\?\//u, "").toLowerCase();
764
+ return normalize(left) === normalize(right);
545
765
  };
546
766
  const acquireLock = async (lockFile, timeoutMs) => {
547
767
  const deadline = Date.now() + timeoutMs;
@@ -573,6 +793,7 @@ const isProcessAlive = (pid) => {
573
793
  };
574
794
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
575
795
  const isNodeError = (error) => error instanceof Error && "code" in error;
796
+ const isRecord = (value) => typeof value === "object" && value !== null;
576
797
  //#endregion
577
798
  //#region src/daemon/package-version.ts
578
799
  const readPackageVersion = async (packageJsonUrl) => {
@@ -632,15 +853,19 @@ const connectLocalBroker = async (options = {}) => {
632
853
  const endpoint = options.endpoint ?? paths.endpoint;
633
854
  const autoStart = options.autoStart ?? endpoint === paths.endpoint;
634
855
  if (autoStart && endpoint !== paths.endpoint) throw new Error("local broker autoStart requires the derived same-version endpoint");
635
- if (autoStart) {
636
- const cliEntrypoint = options.cliEntrypoint ?? resolveCliEntrypoint();
637
- await startDaemon({
638
- paths,
639
- driver: options.daemonDriver ?? createNodeDaemonDriver(cliEntrypoint)
640
- });
641
- }
856
+ const cliEntrypoint = options.cliEntrypoint ?? resolveCliEntrypoint();
857
+ const driver = options.daemonDriver ?? createNodeDaemonDriver(cliEntrypoint);
858
+ const expectedBrokerArtifactIdentity = autoStart ? (await startDaemon({
859
+ paths,
860
+ driver
861
+ })).broker.artifactIdentity : options.expectedBrokerArtifactIdentity ?? (await driver.resolveBroker(paths)).artifactIdentity;
642
862
  const connection = new LocalBrokerConnection(await connectSocket(endpoint), endpoint, callerLabel);
643
- await connection.init(clientVersion, options.protocolVersion ?? PROTOCOL_VERSION$1);
863
+ try {
864
+ await connection.init(clientVersion, options.protocolVersion ?? PROTOCOL_VERSION$1, expectedBrokerArtifactIdentity);
865
+ } catch (error) {
866
+ await connection.close();
867
+ throw error;
868
+ }
644
869
  return connection;
645
870
  };
646
871
  const normalizeAppIdentityField = (value) => {
@@ -671,7 +896,7 @@ var LocalBrokerConnection = class {
671
896
  this.rejectAll(/* @__PURE__ */ new Error("broker connection closed"));
672
897
  });
673
898
  }
674
- async init(clientVersion, protocolVersion) {
899
+ async init(clientVersion, protocolVersion, expectedBrokerArtifactIdentity) {
675
900
  const ready = new Promise((resolve, reject) => {
676
901
  this.ready = {
677
902
  resolve,
@@ -684,6 +909,7 @@ var LocalBrokerConnection = class {
684
909
  clientVersion
685
910
  });
686
911
  const frame = await ready;
912
+ if (!brokerArtifactIdentityEquals(frame.brokerArtifactIdentity, expectedBrokerArtifactIdentity)) throw new Error(`broker artifact identity mismatch: expected=${JSON.stringify(expectedBrokerArtifactIdentity)} actual=${JSON.stringify(frame.brokerArtifactIdentity)}`);
687
913
  this.sessionId = frame.sessionId;
688
914
  }
689
915
  async request(frame) {
@@ -778,6 +1004,7 @@ const responseRequestId = (frame) => {
778
1004
  switch (frame.type) {
779
1005
  case "app-created":
780
1006
  case "default-app":
1007
+ case "app-identity":
781
1008
  case "tray-created":
782
1009
  case "tray-bounds":
783
1010
  case "ack":
@@ -914,10 +1141,22 @@ const registerClickId = (state, id) => {
914
1141
  //#endregion
915
1142
  //#region src/sdk.ts
916
1143
  const createTray = async (options, runtimeOptions = {}) => {
917
- const connection = await connectLocalBroker(runtimeOptions);
918
1144
  const normalized = normalizeCreateTrayOptions(options);
919
- const tray = await createClient(connection).createTray(normalized.options);
920
- return wrapCreateTrayHandle(tray, { menuUnsubscribe: bindMenuClickHandlers(tray, normalized.menuHandlers) });
1145
+ const connection = await connectLocalBroker(runtimeOptions);
1146
+ try {
1147
+ const tray = await createClient(connection, { appOptions: {
1148
+ ...runtimeOptions.appName === void 0 ? {} : { name: runtimeOptions.appName },
1149
+ ...runtimeOptions.appIcon === void 0 ? {} : { icon: runtimeOptions.appIcon }
1150
+ } }).createTray(normalized.options);
1151
+ return wrapCreateTrayHandle(tray, {
1152
+ closeConnection: () => connection.close(),
1153
+ destroyPromise: void 0,
1154
+ menuUnsubscribe: bindMenuClickHandlers(tray, normalized.menuHandlers)
1155
+ });
1156
+ } catch (error) {
1157
+ await connection.close().catch(noop);
1158
+ throw error;
1159
+ }
921
1160
  };
922
1161
  const normalizeCreateTrayOptions = (options) => {
923
1162
  const protocolOptions = {
@@ -951,9 +1190,16 @@ const wrapCreateTrayHandle = (tray, state) => {
951
1190
  return wrapCreateTrayHandle(tray.extend(extension, options), state);
952
1191
  },
953
1192
  async destroy() {
954
- state.menuUnsubscribe();
955
- state.menuUnsubscribe = noop;
956
- await tray.destroy();
1193
+ state.destroyPromise ??= (async () => {
1194
+ state.menuUnsubscribe();
1195
+ state.menuUnsubscribe = noop;
1196
+ try {
1197
+ await tray.destroy();
1198
+ } finally {
1199
+ await state.closeConnection();
1200
+ }
1201
+ })();
1202
+ await state.destroyPromise;
957
1203
  }
958
1204
  };
959
1205
  };
@@ -965,6 +1211,6 @@ const bindMenuClickHandlers = (tray, handlers) => {
965
1211
  };
966
1212
  const noop = () => {};
967
1213
  //#endregion
968
- export { PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion };
1214
+ export { NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact };
969
1215
 
970
1216
  //# sourceMappingURL=index.mjs.map