opentray 0.17.0 → 0.19.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/README.md +44 -1
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +9 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +274 -64
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
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";
|
|
3
|
+
import { access, appendFile, link, mkdir, open, readFile, realpath, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { createConnection } from "node:net";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { clearDarwinAppBundleOwner, ensureDarwinAppBundle, resolveDefaultDarwinAppBundlePath, resolveOpenTrayPackageIdentity, writeDarwinAppBundleOwner } from "@opentray/packaging";
|
|
8
|
+
import { clearDarwinAppBundleOwner, convergeDarwinAppBundleIdentity, ensureDarwinAppBundle, resolveDefaultDarwinAppBundlePath, resolveOpenTrayPackageIdentity, updateDarwinAppLaunchDescriptor, writeDarwinAppBundleOwner } from "@opentray/packaging";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
10
|
import { constants } from "node:fs";
|
|
10
11
|
import { spawn } from "node:child_process";
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
12
|
//#region src/app-icon.ts
|
|
13
13
|
const DEFAULT_APP_ICON_VARIANT = "default";
|
|
14
14
|
const APP_ICON_VARIANT_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
@@ -221,7 +221,7 @@ const matchesFormat = (bytes, format) => {
|
|
|
221
221
|
const hasPrefix = (bytes, prefix) => bytes.length >= prefix.length && prefix.every((byte, index) => bytes[index] === byte);
|
|
222
222
|
const assertExactKeys = (record, expected, platform, optional = []) => {
|
|
223
223
|
const actual = Object.keys(record);
|
|
224
|
-
const allowed = new Set([...expected, ...optional]);
|
|
224
|
+
const allowed = /* @__PURE__ */ new Set([...expected, ...optional]);
|
|
225
225
|
const unexpected = actual.find((key) => !allowed.has(key));
|
|
226
226
|
if (unexpected !== void 0) throw new InvalidAppIconError(`unexpected asset field ${unexpected}`, platform);
|
|
227
227
|
const missing = expected.find((key) => !(key in record));
|
|
@@ -272,8 +272,9 @@ const resolveNativeExtensionArtifact = async (artifact, platform = process.platf
|
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
274
|
const manifest = await readPlatformPackageManifest(platformPackageJsonPath, target, packageTarget.packageName);
|
|
275
|
+
const libraryPath = join(dirname(platformPackageJsonPath), packageTarget.libraryPath);
|
|
275
276
|
return {
|
|
276
|
-
path: await resolveAccessibleLibrary(
|
|
277
|
+
path: await resolveAccessibleLibrary(libraryPath, target, packageTarget.packageName),
|
|
277
278
|
packageName: packageTarget.packageName,
|
|
278
279
|
packageVersion: manifest.version,
|
|
279
280
|
expectedIdentity,
|
|
@@ -377,12 +378,13 @@ function createClient(transport, options = {}) {
|
|
|
377
378
|
return { async createTray(options) {
|
|
378
379
|
const { app } = await ensureApp(options);
|
|
379
380
|
const requestId = nextRequestId();
|
|
380
|
-
const
|
|
381
|
+
const response = await transport.request({
|
|
381
382
|
type: "create-tray",
|
|
382
383
|
requestId,
|
|
383
384
|
app,
|
|
384
385
|
tray: options
|
|
385
|
-
})
|
|
386
|
+
});
|
|
387
|
+
const frame = expectResponse(response, requestId, "tray-created");
|
|
386
388
|
return createTrayHandle(transport, app.appId, frame.trayId ?? options.id, nextRequestId);
|
|
387
389
|
} };
|
|
388
390
|
}
|
|
@@ -398,47 +400,51 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
|
|
|
398
400
|
app: createAppHandle(transport, appId, nextRequestId),
|
|
399
401
|
async getBounds() {
|
|
400
402
|
const requestId = nextRequestId();
|
|
401
|
-
|
|
403
|
+
const response = await transport.request({
|
|
402
404
|
type: "get-tray-bounds",
|
|
403
405
|
requestId,
|
|
404
406
|
appId,
|
|
405
407
|
trayId
|
|
406
|
-
})
|
|
408
|
+
});
|
|
409
|
+
return expectResponse(response, requestId, "tray-bounds").bounds;
|
|
407
410
|
},
|
|
408
411
|
async setMenu(menu) {
|
|
409
412
|
const requestId = nextRequestId();
|
|
410
|
-
|
|
413
|
+
const response = await transport.request({
|
|
411
414
|
type: "set-tray-menu",
|
|
412
415
|
requestId,
|
|
413
416
|
appId,
|
|
414
417
|
trayId,
|
|
415
418
|
menu
|
|
416
|
-
})
|
|
419
|
+
});
|
|
420
|
+
expectResponse(response, requestId, "ack");
|
|
417
421
|
},
|
|
418
422
|
async setTooltip(tooltip) {
|
|
419
423
|
const requestId = nextRequestId();
|
|
420
|
-
|
|
424
|
+
const response = await transport.request({
|
|
421
425
|
type: "set-tray-tooltip",
|
|
422
426
|
requestId,
|
|
423
427
|
appId,
|
|
424
428
|
trayId,
|
|
425
429
|
tooltip
|
|
426
|
-
})
|
|
430
|
+
});
|
|
431
|
+
expectResponse(response, requestId, "ack");
|
|
427
432
|
},
|
|
428
433
|
async setIcon(icon) {
|
|
429
434
|
const requestId = nextRequestId();
|
|
430
|
-
|
|
435
|
+
const response = await transport.request({
|
|
431
436
|
type: "set-tray-icon",
|
|
432
437
|
requestId,
|
|
433
438
|
appId,
|
|
434
439
|
trayId,
|
|
435
440
|
icon
|
|
436
|
-
})
|
|
441
|
+
});
|
|
442
|
+
expectResponse(response, requestId, "ack");
|
|
437
443
|
},
|
|
438
444
|
async loadExtension(options) {
|
|
439
445
|
const requestId = nextRequestId();
|
|
440
446
|
const resolved = await resolveNativeExtensionArtifact(options.artifact);
|
|
441
|
-
|
|
447
|
+
const response = await transport.request({
|
|
442
448
|
type: "load-ext",
|
|
443
449
|
requestId,
|
|
444
450
|
appId,
|
|
@@ -446,7 +452,8 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
|
|
|
446
452
|
path: resolved.path,
|
|
447
453
|
expectedIdentity: resolved.expectedIdentity,
|
|
448
454
|
...options.mountId === void 0 ? {} : { mountId: options.mountId }
|
|
449
|
-
})
|
|
455
|
+
});
|
|
456
|
+
expectResponse(response, requestId, "ack");
|
|
450
457
|
},
|
|
451
458
|
async commandExtension(ext, data) {
|
|
452
459
|
await this.requestExtension(ext, data);
|
|
@@ -474,12 +481,13 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
|
|
|
474
481
|
},
|
|
475
482
|
async destroy() {
|
|
476
483
|
const requestId = nextRequestId();
|
|
477
|
-
|
|
484
|
+
const response = await transport.request({
|
|
478
485
|
type: "destroy-tray",
|
|
479
486
|
requestId,
|
|
480
487
|
appId,
|
|
481
488
|
trayId
|
|
482
|
-
})
|
|
489
|
+
});
|
|
490
|
+
expectResponse(response, requestId, "ack");
|
|
483
491
|
}
|
|
484
492
|
};
|
|
485
493
|
if (!isOpenTrayEventSource(transport)) return handle;
|
|
@@ -517,6 +525,11 @@ const attachEventfulTrayHandle = (handle, source, appId, nextMountId) => {
|
|
|
517
525
|
onTrayDoubleClick(handler) {
|
|
518
526
|
return listen("trayDoubleClick", handler);
|
|
519
527
|
},
|
|
528
|
+
onAppReopenRequested(handler) {
|
|
529
|
+
return source.onEvent((frame) => {
|
|
530
|
+
if (frame.type === "app-event" && frame.event.type === "reopenRequested" && frame.event.appId === appId) handler(frame.event);
|
|
531
|
+
});
|
|
532
|
+
},
|
|
520
533
|
extend(extension, options) {
|
|
521
534
|
const context = createTrayExtensionContext(eventful, appId, extension, options, nextMountId);
|
|
522
535
|
const capability = extension.extend(eventful, context, options);
|
|
@@ -529,19 +542,21 @@ const isOwnedTrayEvent = (event, appId, trayId) => event.type !== "ready" && eve
|
|
|
529
542
|
const isOpenTrayEventSource = (transport) => "onEvent" in transport && typeof transport.onEvent === "function";
|
|
530
543
|
const resolveDefaultAppRef = async (transport, nextRequestId) => {
|
|
531
544
|
const requestId = nextRequestId();
|
|
532
|
-
|
|
545
|
+
const response = await transport.request({
|
|
533
546
|
type: "resolve-default-app",
|
|
534
547
|
requestId
|
|
535
|
-
})
|
|
548
|
+
});
|
|
549
|
+
return expectResponse(response, requestId, "default-app").app;
|
|
536
550
|
};
|
|
537
551
|
const createAppHandle = (transport, appId, nextRequestId) => {
|
|
538
552
|
const requestIdentity = async () => {
|
|
539
553
|
const requestId = nextRequestId();
|
|
540
|
-
|
|
554
|
+
const response = await transport.request({
|
|
541
555
|
type: "get-app-identity",
|
|
542
556
|
requestId,
|
|
543
557
|
appId
|
|
544
|
-
})
|
|
558
|
+
});
|
|
559
|
+
return expectResponse(response, requestId, "app-identity").identity;
|
|
545
560
|
};
|
|
546
561
|
return {
|
|
547
562
|
appId,
|
|
@@ -550,12 +565,13 @@ const createAppHandle = (transport, appId, nextRequestId) => {
|
|
|
550
565
|
},
|
|
551
566
|
async setName(name) {
|
|
552
567
|
const requestId = nextRequestId();
|
|
553
|
-
|
|
568
|
+
const response = await transport.request({
|
|
554
569
|
type: "set-app-name",
|
|
555
570
|
requestId,
|
|
556
571
|
appId,
|
|
557
572
|
name
|
|
558
|
-
})
|
|
573
|
+
});
|
|
574
|
+
expectResponse(response, requestId, "ack");
|
|
559
575
|
return (await requestIdentity()).appName;
|
|
560
576
|
},
|
|
561
577
|
async getAppIcon() {
|
|
@@ -570,23 +586,25 @@ const createAppHandle = (transport, appId, nextRequestId) => {
|
|
|
570
586
|
if (identity.appIcon === void 0) selectAppIconVariant([], iconOrVariant);
|
|
571
587
|
else selectAppIconVariant(identity.appIcon, iconOrVariant);
|
|
572
588
|
const requestId = nextRequestId();
|
|
573
|
-
|
|
589
|
+
const response = await transport.request({
|
|
574
590
|
type: "set-app-icon-variant",
|
|
575
591
|
requestId,
|
|
576
592
|
appId,
|
|
577
593
|
variant: iconOrVariant
|
|
578
|
-
})
|
|
594
|
+
});
|
|
595
|
+
expectResponse(response, requestId, "ack");
|
|
579
596
|
return;
|
|
580
597
|
}
|
|
581
598
|
if (iconOrVariant !== null) await validateAppIcon(iconOrVariant);
|
|
582
599
|
const appIcon = iconOrVariant === null ? null : normalizeAppIcon(iconOrVariant);
|
|
583
600
|
const requestId = nextRequestId();
|
|
584
|
-
|
|
601
|
+
const response = await transport.request({
|
|
585
602
|
type: "set-app-icon",
|
|
586
603
|
requestId,
|
|
587
604
|
appId,
|
|
588
605
|
appIcon
|
|
589
|
-
})
|
|
606
|
+
});
|
|
607
|
+
expectResponse(response, requestId, "ack");
|
|
590
608
|
}
|
|
591
609
|
};
|
|
592
610
|
};
|
|
@@ -655,6 +673,7 @@ const requestIdOf = (frame) => {
|
|
|
655
673
|
case "error": return frame.requestId;
|
|
656
674
|
case "ready":
|
|
657
675
|
case "event":
|
|
676
|
+
case "app-event":
|
|
658
677
|
case "ext-event": return;
|
|
659
678
|
}
|
|
660
679
|
};
|
|
@@ -768,14 +787,16 @@ const resolveBrokerCommand = async (paths, options = {}) => {
|
|
|
768
787
|
if (platform !== "darwin") return commandForBinary(installed.binary, paths);
|
|
769
788
|
if (options.appBundle === void 0) return commandForBinary(installed.binary, paths);
|
|
770
789
|
if (installed.carrierTemplate === void 0) throw missingDarwinCarrierError(target, installed, arch);
|
|
771
|
-
|
|
790
|
+
const binary = await ensureDarwinBundle(options, paths, installed.binary, installed.carrierTemplate);
|
|
791
|
+
return commandForBinary(binary, paths);
|
|
772
792
|
}
|
|
773
793
|
const sourceDir = options.sourceDir ?? dirname(fileURLToPath(sourceUrl));
|
|
774
794
|
const workspaceRoot = await (options.findWorkspaceRoot ?? findWorkspaceRoot)(sourceDir);
|
|
775
795
|
if (workspaceRoot !== void 0) {
|
|
776
796
|
const binary = await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot, paths, platform);
|
|
777
797
|
if (platform !== "darwin") return commandForBinary(binary, paths);
|
|
778
|
-
|
|
798
|
+
const carrierBinary = options.appBundle === void 0 ? binary : await ensureDarwinBundle(options, paths, binary, await (options.ensureDevDarwinCarrierTemplate ?? ensureDevDarwinCarrierTemplate)(workspaceRoot, paths));
|
|
799
|
+
return commandForBinary(carrierBinary, paths);
|
|
779
800
|
}
|
|
780
801
|
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, {
|
|
781
802
|
packageName: target.packageName,
|
|
@@ -928,7 +949,10 @@ function isNodeError$1(error) {
|
|
|
928
949
|
//#endregion
|
|
929
950
|
//#region src/daemon/lifecycle.ts
|
|
930
951
|
const DAEMON_STDIO_ENV = "OPENTRAY_DAEMON_STDIO";
|
|
931
|
-
const
|
|
952
|
+
const DAEMON_POLL_INTERVAL_MS = 50;
|
|
953
|
+
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 1e4;
|
|
954
|
+
const DEFAULT_DAEMON_LOCK_TIMEOUT_MS = 15e3;
|
|
955
|
+
const ABANDONED_RECLAIM_CLAIM_MS = 1e3;
|
|
932
956
|
const createNodeDaemonDriver = (cliEntrypoint, options = {}) => ({
|
|
933
957
|
async resolveBroker(paths) {
|
|
934
958
|
return resolveBrokerArtifact(paths, {
|
|
@@ -941,19 +965,71 @@ const createNodeDaemonDriver = (cliEntrypoint, options = {}) => ({
|
|
|
941
965
|
return isProcessAlive(pid);
|
|
942
966
|
},
|
|
943
967
|
async spawnBroker(paths, broker) {
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
968
|
+
const env = options.env ?? process.env;
|
|
969
|
+
const stdio = resolveBrokerStdio(env[DAEMON_STDIO_ENV]);
|
|
970
|
+
if (stdio === "log") await mkdir(paths.runtimeDir, { recursive: true });
|
|
971
|
+
const logHandle = stdio === "log" ? await open(paths.brokerLog, "a") : void 0;
|
|
972
|
+
if (logHandle !== void 0) await logHandle.appendFile(`${JSON.stringify({
|
|
973
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
974
|
+
event: "broker-spawn",
|
|
975
|
+
command: broker.command,
|
|
976
|
+
cwd: broker.cwd ?? process.cwd(),
|
|
977
|
+
appId: paths.appId,
|
|
978
|
+
appName: paths.appName
|
|
979
|
+
})}\n`, "utf8");
|
|
980
|
+
if (isAbsolute(broker.command)) try {
|
|
981
|
+
await access(broker.command, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
982
|
+
} catch (error) {
|
|
983
|
+
if (stdio === "log") await appendFile(paths.brokerLog, `${JSON.stringify({
|
|
984
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
985
|
+
event: "broker-spawn-error",
|
|
986
|
+
command: broker.command,
|
|
987
|
+
error: error instanceof Error ? error.message : String(error)
|
|
988
|
+
})}\n`, "utf8").catch(() => void 0);
|
|
989
|
+
await logHandle?.close();
|
|
990
|
+
return 0;
|
|
991
|
+
}
|
|
992
|
+
let child;
|
|
993
|
+
try {
|
|
994
|
+
child = spawn(broker.command, broker.args, {
|
|
995
|
+
cwd: broker.cwd,
|
|
996
|
+
detached: true,
|
|
997
|
+
env: {
|
|
998
|
+
...env,
|
|
999
|
+
OPENTRAY_DAEMON_HOME: paths.homeDir,
|
|
1000
|
+
OPENTRAY_DAEMON_PACKAGE_VERSION: paths.packageVersion,
|
|
1001
|
+
OPENTRAY_DAEMON_CLI_ENTRYPOINT: cliEntrypoint,
|
|
1002
|
+
OPENTRAY_DAEMON_APP_ID: paths.appId,
|
|
1003
|
+
OPENTRAY_DAEMON_APP_NAME: paths.appName,
|
|
1004
|
+
OPENTRAY_DAEMON_CALLER_LABEL: paths.callerLabel
|
|
1005
|
+
},
|
|
1006
|
+
stdio: logHandle === void 0 ? stdio === "log" ? "ignore" : stdio : [
|
|
1007
|
+
"ignore",
|
|
1008
|
+
logHandle.fd,
|
|
1009
|
+
logHandle.fd
|
|
1010
|
+
]
|
|
1011
|
+
});
|
|
1012
|
+
} finally {
|
|
1013
|
+
await logHandle?.close();
|
|
1014
|
+
}
|
|
1015
|
+
child.on("error", (error) => {
|
|
1016
|
+
if (stdio !== "log") return;
|
|
1017
|
+
appendFile(paths.brokerLog, `${JSON.stringify({
|
|
1018
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1019
|
+
event: "broker-spawn-error",
|
|
1020
|
+
command: broker.command,
|
|
1021
|
+
error: error.message
|
|
1022
|
+
})}\n`, "utf8").catch(() => void 0);
|
|
1023
|
+
});
|
|
1024
|
+
child.once("exit", (code, signal) => {
|
|
1025
|
+
if (stdio !== "log") return;
|
|
1026
|
+
appendFile(paths.brokerLog, `${JSON.stringify({
|
|
1027
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1028
|
+
event: "broker-exit",
|
|
1029
|
+
command: broker.command,
|
|
1030
|
+
code,
|
|
1031
|
+
signal
|
|
1032
|
+
})}\n`, "utf8").catch(() => void 0);
|
|
957
1033
|
});
|
|
958
1034
|
child.unref();
|
|
959
1035
|
const pid = child.pid ?? 0;
|
|
@@ -966,11 +1042,13 @@ const createNodeDaemonDriver = (cliEntrypoint, options = {}) => ({
|
|
|
966
1042
|
}
|
|
967
1043
|
});
|
|
968
1044
|
const resolveBrokerStdio = (value) => {
|
|
969
|
-
|
|
1045
|
+
if (value === "inherit") return "inherit";
|
|
1046
|
+
if (value === "ignore") return "ignore";
|
|
1047
|
+
return "log";
|
|
970
1048
|
};
|
|
971
|
-
const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS }) => {
|
|
1049
|
+
const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS, readinessTimeoutMs = DEFAULT_DAEMON_READY_TIMEOUT_MS }) => {
|
|
972
1050
|
await mkdir(paths.runtimeDir, { recursive: true });
|
|
973
|
-
const lock = await
|
|
1051
|
+
const lock = await acquireDaemonLock(paths.lockFile, lockTimeoutMs);
|
|
974
1052
|
try {
|
|
975
1053
|
const existingPid = await readPid(paths.pidFile);
|
|
976
1054
|
let existingAlive = existingPid !== void 0 && await driver.isAlive(existingPid);
|
|
@@ -986,7 +1064,8 @@ const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_
|
|
|
986
1064
|
} else throw error;
|
|
987
1065
|
}
|
|
988
1066
|
if (existingPid !== void 0 && existingAlive) {
|
|
989
|
-
|
|
1067
|
+
const ready = await readReadyMetadata(paths.readyFile);
|
|
1068
|
+
if (readyMatchesBroker(ready, paths, existingPid, broker)) return {
|
|
990
1069
|
status: "already-running",
|
|
991
1070
|
pid: existingPid,
|
|
992
1071
|
paths,
|
|
@@ -1000,7 +1079,7 @@ const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_
|
|
|
1000
1079
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("daemon broker process did not expose a valid pid");
|
|
1001
1080
|
await writeFile(paths.pidFile, `${pid}\n`, "utf8");
|
|
1002
1081
|
try {
|
|
1003
|
-
await waitForReadyFile(paths, driver, pid, broker);
|
|
1082
|
+
await waitForReadyFile(paths, driver, pid, broker, readinessTimeoutMs);
|
|
1004
1083
|
} catch (error) {
|
|
1005
1084
|
if (await driver.isAlive(pid)) {
|
|
1006
1085
|
await driver.stop(pid);
|
|
@@ -1037,19 +1116,22 @@ const cleanupRuntimeFiles = async (paths) => {
|
|
|
1037
1116
|
const waitUntilStopped = async (driver, pid) => {
|
|
1038
1117
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
1039
1118
|
if (!await driver.isAlive(pid)) return true;
|
|
1040
|
-
await sleep(
|
|
1119
|
+
await sleep(DAEMON_POLL_INTERVAL_MS);
|
|
1041
1120
|
}
|
|
1042
1121
|
return false;
|
|
1043
1122
|
};
|
|
1044
|
-
const waitForReadyFile = async (paths, driver, pid, broker) => {
|
|
1045
|
-
|
|
1123
|
+
const waitForReadyFile = async (paths, driver, pid, broker, timeoutMs) => {
|
|
1124
|
+
const deadline = Date.now() + timeoutMs;
|
|
1125
|
+
while (true) {
|
|
1046
1126
|
const ready = await readReadyMetadata(paths.readyFile);
|
|
1047
1127
|
if (readyMatchesBroker(ready, paths, pid, broker)) return;
|
|
1048
1128
|
if (ready !== void 0) throw new Error(`daemon readiness artifact identity mismatch: expected=${JSON.stringify(broker.artifactIdentity)} actual=${JSON.stringify(ready.brokerArtifactIdentity)}`);
|
|
1049
|
-
if (!await driver.isAlive(pid)) throw new Error(`daemon broker exited before readiness: pid=${pid}; readyFile=${paths.readyFile}; set ${DAEMON_STDIO_ENV}=inherit to
|
|
1050
|
-
|
|
1129
|
+
if (!await driver.isAlive(pid)) throw new Error(`daemon broker exited before readiness: pid=${pid}; readyFile=${paths.readyFile}; brokerLog=${paths.brokerLog}; set ${DAEMON_STDIO_ENV}=inherit to mirror broker stderr`);
|
|
1130
|
+
const remainingMs = deadline - Date.now();
|
|
1131
|
+
if (remainingMs <= 0) break;
|
|
1132
|
+
await sleep(Math.min(DAEMON_POLL_INTERVAL_MS, remainingMs));
|
|
1051
1133
|
}
|
|
1052
|
-
throw new Error(`timed out waiting for daemon readiness
|
|
1134
|
+
throw new Error(`timed out waiting for daemon readiness after ${timeoutMs}ms: pid=${pid}; readyFile=${paths.readyFile}; brokerLog=${paths.brokerLog}; set ${DAEMON_STDIO_ENV}=inherit to mirror broker stderr`);
|
|
1053
1135
|
};
|
|
1054
1136
|
const readReadyMetadata = async (readyFile) => {
|
|
1055
1137
|
let value;
|
|
@@ -1080,19 +1162,25 @@ const executablePathsEqual = (left, right, platform = process.platform) => {
|
|
|
1080
1162
|
const normalize = (value) => value.replaceAll("\\", "/").replace(/^\/\/?\?\//u, "").toLowerCase();
|
|
1081
1163
|
return normalize(left) === normalize(right);
|
|
1082
1164
|
};
|
|
1083
|
-
const
|
|
1165
|
+
const acquireDaemonLock = async (lockFile, timeoutMs) => {
|
|
1084
1166
|
const deadline = Date.now() + timeoutMs;
|
|
1167
|
+
const owner = `${JSON.stringify({
|
|
1168
|
+
pid: process.pid,
|
|
1169
|
+
token: randomUUID()
|
|
1170
|
+
})}\n`;
|
|
1085
1171
|
while (Date.now() <= deadline) try {
|
|
1086
1172
|
const handle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
1087
|
-
await handle.writeFile(
|
|
1173
|
+
await handle.writeFile(owner, "utf8");
|
|
1088
1174
|
return { async release() {
|
|
1089
1175
|
await handle.close();
|
|
1176
|
+
if (await readLockSource(lockFile) !== owner) return;
|
|
1090
1177
|
await unlink(lockFile).catch((error) => {
|
|
1091
1178
|
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
1092
1179
|
});
|
|
1093
1180
|
} };
|
|
1094
1181
|
} catch (error) {
|
|
1095
1182
|
if (isNodeError(error) && error.code === "EEXIST") {
|
|
1183
|
+
if (await reclaimDeadLockOwner(lockFile)) continue;
|
|
1096
1184
|
await sleep(25);
|
|
1097
1185
|
continue;
|
|
1098
1186
|
}
|
|
@@ -1100,12 +1188,72 @@ const acquireLock = async (lockFile, timeoutMs) => {
|
|
|
1100
1188
|
}
|
|
1101
1189
|
throw new Error(`timed out acquiring daemon lock: ${lockFile}`);
|
|
1102
1190
|
};
|
|
1191
|
+
const reclaimDeadLockOwner = async (lockFile) => {
|
|
1192
|
+
const observed = await readLockSource(lockFile);
|
|
1193
|
+
if (observed === void 0) return true;
|
|
1194
|
+
const owner = parseLockOwner(observed);
|
|
1195
|
+
if (owner === void 0 || isProcessAlive(owner.pid)) return false;
|
|
1196
|
+
const claimFile = `${lockFile}.reclaim-${createHash("sha256").update(observed).digest("hex").slice(0, 16)}`;
|
|
1197
|
+
try {
|
|
1198
|
+
await link(lockFile, claimFile);
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
if (isNodeError(error) && error.code === "ENOENT") return true;
|
|
1201
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
1202
|
+
await clearAbandonedReclaimClaim(claimFile);
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
if (await readLockSource(lockFile) !== observed || isProcessAlive(owner.pid)) return false;
|
|
1209
|
+
try {
|
|
1210
|
+
await unlink(lockFile);
|
|
1211
|
+
return true;
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (isNodeError(error) && error.code === "ENOENT") return true;
|
|
1214
|
+
throw error;
|
|
1215
|
+
}
|
|
1216
|
+
} finally {
|
|
1217
|
+
await unlink(claimFile).catch((error) => {
|
|
1218
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
const clearAbandonedReclaimClaim = async (claimFile) => {
|
|
1223
|
+
try {
|
|
1224
|
+
const claim = await stat(claimFile);
|
|
1225
|
+
if (Date.now() - claim.ctimeMs < ABANDONED_RECLAIM_CLAIM_MS) return;
|
|
1226
|
+
await unlink(claimFile);
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
const readLockSource = async (lockFile) => {
|
|
1232
|
+
try {
|
|
1233
|
+
return await readFile(lockFile, "utf8");
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
const parseLockOwner = (source) => {
|
|
1240
|
+
const legacyPid = Number(source.trim());
|
|
1241
|
+
if (Number.isInteger(legacyPid) && legacyPid > 0) return { pid: legacyPid };
|
|
1242
|
+
let value;
|
|
1243
|
+
try {
|
|
1244
|
+
value = JSON.parse(source);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (!isRecord(value) || typeof value.pid !== "number" || !Number.isInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || value.token.length === 0) return;
|
|
1249
|
+
return { pid: value.pid };
|
|
1250
|
+
};
|
|
1103
1251
|
const isProcessAlive = (pid) => {
|
|
1104
1252
|
try {
|
|
1105
1253
|
process.kill(pid, 0);
|
|
1106
1254
|
return true;
|
|
1107
|
-
} catch {
|
|
1108
|
-
return
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
return !(isNodeError(error) && error.code === "ESRCH");
|
|
1109
1257
|
}
|
|
1110
1258
|
};
|
|
1111
1259
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1144,7 +1292,8 @@ const resolveDaemonPaths = ({ homeDir, packageVersion, callerLabel, appId, appNa
|
|
|
1144
1292
|
endpoint,
|
|
1145
1293
|
pidFile: `${runtimeDir}/broker.pid`,
|
|
1146
1294
|
lockFile: `${runtimeDir}/broker.lock`,
|
|
1147
|
-
readyFile: `${runtimeDir}/ready.json
|
|
1295
|
+
readyFile: `${runtimeDir}/ready.json`,
|
|
1296
|
+
brokerLog: `${runtimeDir}/broker.log`
|
|
1148
1297
|
};
|
|
1149
1298
|
};
|
|
1150
1299
|
const normalizeAppIdentityField$1 = (value, fallback) => {
|
|
@@ -1188,12 +1337,37 @@ const connectLocalBroker = async (options = {}) => {
|
|
|
1188
1337
|
const connection = new LocalBrokerConnection(await connectSocket(endpoint), endpoint, callerLabel);
|
|
1189
1338
|
try {
|
|
1190
1339
|
await connection.init(clientVersion, options.protocolVersion ?? PROTOCOL_VERSION$1, expectedBrokerArtifactIdentity);
|
|
1340
|
+
if (appBundle !== void 0 && options.appLaunch !== void 0) {
|
|
1341
|
+
await updateDarwinAppLaunchDescriptor(appBundle.path, options.appLaunch);
|
|
1342
|
+
const convergence = await convergeDarwinAppBundleIdentity({
|
|
1343
|
+
currentBundlePath: appBundle.path,
|
|
1344
|
+
appId: paths.appId,
|
|
1345
|
+
managedAppsRoot: resolve(paths.homeDir, ".opentray/apps"),
|
|
1346
|
+
legacyRuntimeRoot: resolve(paths.homeDir, ".opentray"),
|
|
1347
|
+
legacyBundlePaths: [resolve(paths.runtimeDir, "darwin-carrier/OpenTray.app")]
|
|
1348
|
+
});
|
|
1349
|
+
await appendBrokerDiagnostic(paths.brokerLog, {
|
|
1350
|
+
event: "bundle-identity-convergence",
|
|
1351
|
+
removed: convergence.removed,
|
|
1352
|
+
skippedLiveOwner: convergence.skippedLiveOwner,
|
|
1353
|
+
failedUnregister: convergence.failedUnregister
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1191
1356
|
} catch (error) {
|
|
1192
1357
|
await connection.close();
|
|
1193
1358
|
throw error;
|
|
1194
1359
|
}
|
|
1195
1360
|
return connection;
|
|
1196
1361
|
};
|
|
1362
|
+
const appendBrokerDiagnostic = async (path, value) => {
|
|
1363
|
+
try {
|
|
1364
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1365
|
+
await appendFile(path, `${JSON.stringify({
|
|
1366
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1367
|
+
...value
|
|
1368
|
+
})}\n`, "utf8");
|
|
1369
|
+
} catch {}
|
|
1370
|
+
};
|
|
1197
1371
|
const resolveDarwinAppBundleOptions = (configured, paths, packageIdentity) => {
|
|
1198
1372
|
if (packageIdentity === void 0) throw new Error("Darwin app bundle resolution requires the caller package identity");
|
|
1199
1373
|
const configuredPath = configured?.path;
|
|
@@ -1320,7 +1494,7 @@ var LocalBrokerConnection = class {
|
|
|
1320
1494
|
this.pending.delete(requestId);
|
|
1321
1495
|
return;
|
|
1322
1496
|
}
|
|
1323
|
-
if (frame.type === "event" || frame.type === "ext-event") for (const listener of this.listeners) listener(frame);
|
|
1497
|
+
if (frame.type === "event" || frame.type === "app-event" || frame.type === "ext-event") for (const listener of this.listeners) listener(frame);
|
|
1324
1498
|
}
|
|
1325
1499
|
write(frame) {
|
|
1326
1500
|
this.socket.write(`${JSON.stringify(frame)}\n`);
|
|
@@ -1352,6 +1526,7 @@ const responseRequestId = (frame) => {
|
|
|
1352
1526
|
case "runtime-host-health": return frame.requestId;
|
|
1353
1527
|
case "ready":
|
|
1354
1528
|
case "event":
|
|
1529
|
+
case "app-event":
|
|
1355
1530
|
case "ext-event":
|
|
1356
1531
|
case "error": return;
|
|
1357
1532
|
}
|
|
@@ -1479,14 +1654,48 @@ const registerClickId = (state, id) => {
|
|
|
1479
1654
|
state.seenClickIds = seen;
|
|
1480
1655
|
};
|
|
1481
1656
|
//#endregion
|
|
1657
|
+
//#region src/app-launch.ts
|
|
1658
|
+
const normalizeAppLaunch = (configured, context = {}) => {
|
|
1659
|
+
const currentCwd = context.cwd ?? process.cwd();
|
|
1660
|
+
if (configured === void 0 || configured === null) return {
|
|
1661
|
+
schemaVersion: 1,
|
|
1662
|
+
command: validateRequiredText(context.execPath ?? process.execPath, "process.execPath"),
|
|
1663
|
+
args: [...(context.argv ?? process.argv).slice(1)].map((arg, index) => validateArg(arg, `process.argv[${index + 1}]`)),
|
|
1664
|
+
cwd: validateRequiredText(currentCwd, "process.cwd()")
|
|
1665
|
+
};
|
|
1666
|
+
const command = validateRequiredText(configured.command, "appLaunch.command");
|
|
1667
|
+
const cwd = resolve(currentCwd, configured.cwd === void 0 ? currentCwd : validateRequiredText(configured.cwd, "appLaunch.cwd"));
|
|
1668
|
+
const args = [...configured.args ?? []].map((arg, index) => validateArg(arg, `appLaunch.args[${index}]`));
|
|
1669
|
+
return {
|
|
1670
|
+
schemaVersion: 1,
|
|
1671
|
+
command: resolvePathLikeCommand(command, currentCwd),
|
|
1672
|
+
args,
|
|
1673
|
+
cwd
|
|
1674
|
+
};
|
|
1675
|
+
};
|
|
1676
|
+
const resolvePathLikeCommand = (command, cwd) => {
|
|
1677
|
+
if (isAbsolute(command) || command.includes("/") || command.includes("\\")) return resolve(cwd, command);
|
|
1678
|
+
return command;
|
|
1679
|
+
};
|
|
1680
|
+
const validateRequiredText = (value, field) => {
|
|
1681
|
+
if (value.trim().length === 0 || value.includes("\0")) throw new Error(`${field} must be a non-empty string without NUL`);
|
|
1682
|
+
return value;
|
|
1683
|
+
};
|
|
1684
|
+
const validateArg = (value, field) => {
|
|
1685
|
+
if (value.includes("\0")) throw new Error(`${field} must be a string without NUL`);
|
|
1686
|
+
return value;
|
|
1687
|
+
};
|
|
1688
|
+
//#endregion
|
|
1482
1689
|
//#region src/sdk.ts
|
|
1483
1690
|
const createTray = async (options, runtimeOptions = {}) => {
|
|
1484
1691
|
if (runtimeOptions.appIcon !== void 0) await validateAppIcon(runtimeOptions.appIcon);
|
|
1485
1692
|
const appIcon = runtimeOptions.appIcon === void 0 ? void 0 : normalizeAppIcon(runtimeOptions.appIcon);
|
|
1486
1693
|
const normalized = normalizeCreateTrayOptions(options);
|
|
1694
|
+
const appLaunch = normalizeAppLaunch(runtimeOptions.appLaunch);
|
|
1487
1695
|
const connection = await connectLocalBroker({
|
|
1488
1696
|
...runtimeOptions,
|
|
1489
|
-
...appIcon === void 0 ? {} : { appIcon }
|
|
1697
|
+
...appIcon === void 0 ? {} : { appIcon },
|
|
1698
|
+
appLaunch
|
|
1490
1699
|
});
|
|
1491
1700
|
try {
|
|
1492
1701
|
const tray = await createClient(connection, { appOptions: {
|
|
@@ -1532,7 +1741,8 @@ const wrapCreateTrayHandle = (tray, state) => {
|
|
|
1532
1741
|
state.menuUnsubscribe = bindMenuClickHandlers(tray, normalized.handlers);
|
|
1533
1742
|
},
|
|
1534
1743
|
extend(extension, options) {
|
|
1535
|
-
|
|
1744
|
+
const extended = tray.extend(extension, options);
|
|
1745
|
+
return wrapCreateTrayHandle(extended, state);
|
|
1536
1746
|
},
|
|
1537
1747
|
async destroy() {
|
|
1538
1748
|
state.destroyPromise ??= (async () => {
|