opentray 0.16.0 → 0.18.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 +108 -3
- package/dist/index.d.mts +30 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +602 -64
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -8
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,240 @@
|
|
|
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";
|
|
4
|
-
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { access, appendFile, link, mkdir, open, readFile, realpath, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
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, convergeDarwinAppBundleIdentity, ensureDarwinAppBundle, resolveDefaultDarwinAppBundlePath, resolveOpenTrayPackageIdentity, updateDarwinAppLaunchDescriptor, writeDarwinAppBundleOwner } from "@opentray/packaging";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
10
|
import { constants } from "node:fs";
|
|
9
11
|
import { spawn } from "node:child_process";
|
|
10
|
-
|
|
12
|
+
//#region src/app-icon.ts
|
|
13
|
+
const DEFAULT_APP_ICON_VARIANT = "default";
|
|
14
|
+
const APP_ICON_VARIANT_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
15
|
+
var InvalidAppIconError = class extends Error {
|
|
16
|
+
code = "OPENTRAY_INVALID_APP_ICON";
|
|
17
|
+
reason;
|
|
18
|
+
constructor(reason, platform = process.platform) {
|
|
19
|
+
super(`invalid appIcon for ${platform}: ${reason}`);
|
|
20
|
+
this.name = "InvalidAppIconError";
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var AppIconVariantNotFoundError = class extends Error {
|
|
25
|
+
code = "OPENTRAY_APP_ICON_VARIANT_NOT_FOUND";
|
|
26
|
+
variant;
|
|
27
|
+
constructor(variant, platform = process.platform) {
|
|
28
|
+
super(`appIcon variant ${variant} is unavailable for ${platform}`);
|
|
29
|
+
this.name = "AppIconVariantNotFoundError";
|
|
30
|
+
this.variant = variant;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Validate an explicit AppIcon, including native file signatures, before broker startup. */
|
|
34
|
+
const validateAppIcon = async (icon, platform = process.platform) => {
|
|
35
|
+
if (!Array.isArray(icon) || icon.length === 0) throw new InvalidAppIconError("the asset array must not be empty", platform);
|
|
36
|
+
const variantPlatforms = /* @__PURE__ */ new Map();
|
|
37
|
+
const seenDarwin = /* @__PURE__ */ new Set();
|
|
38
|
+
const seenWindows = /* @__PURE__ */ new Set();
|
|
39
|
+
const seenLinuxPngSizes = /* @__PURE__ */ new Set();
|
|
40
|
+
const seenLinuxSvg = /* @__PURE__ */ new Set();
|
|
41
|
+
for (const asset of icon) {
|
|
42
|
+
if (!isRecord$1(asset)) throw new InvalidAppIconError("asset must be an object", platform);
|
|
43
|
+
const assetPlatform = asset.platform;
|
|
44
|
+
const format = asset.format;
|
|
45
|
+
const variants = validateVariantNames(asset.variant, platform);
|
|
46
|
+
if (assetPlatform === "darwin" && format === "icns") {
|
|
47
|
+
assertExactKeys(asset, [
|
|
48
|
+
"platform",
|
|
49
|
+
"format",
|
|
50
|
+
"source"
|
|
51
|
+
], platform, ["variant"]);
|
|
52
|
+
rejectDuplicateVariants(seenDarwin, variants, "darwin", platform);
|
|
53
|
+
await validateSource(asset.source, "icns", platform);
|
|
54
|
+
recordVariantPlatforms(variantPlatforms, variants, "darwin");
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (assetPlatform === "windows" && format === "ico") {
|
|
58
|
+
assertExactKeys(asset, [
|
|
59
|
+
"platform",
|
|
60
|
+
"format",
|
|
61
|
+
"source"
|
|
62
|
+
], platform, ["variant"]);
|
|
63
|
+
rejectDuplicateVariants(seenWindows, variants, "windows", platform);
|
|
64
|
+
await validateSource(asset.source, "ico", platform);
|
|
65
|
+
recordVariantPlatforms(variantPlatforms, variants, "windows");
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (assetPlatform === "linux" && format === "png") {
|
|
69
|
+
assertExactKeys(asset, [
|
|
70
|
+
"platform",
|
|
71
|
+
"format",
|
|
72
|
+
"size",
|
|
73
|
+
"source"
|
|
74
|
+
], platform, ["variant"]);
|
|
75
|
+
if (!Number.isInteger(asset.size) || typeof asset.size !== "number" || asset.size <= 0) throw new InvalidAppIconError("Linux PNG assets require a positive integer size", platform);
|
|
76
|
+
for (const variant of variants) {
|
|
77
|
+
const key = `${variant}\0${asset.size}`;
|
|
78
|
+
if (seenLinuxPngSizes.has(key)) throw new InvalidAppIconError(`duplicate Linux PNG size ${asset.size} for variant ${variant}`, platform);
|
|
79
|
+
seenLinuxPngSizes.add(key);
|
|
80
|
+
}
|
|
81
|
+
await validateSource(asset.source, "png", platform);
|
|
82
|
+
recordVariantPlatforms(variantPlatforms, variants, "linux");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (assetPlatform === "linux" && format === "svg") {
|
|
86
|
+
assertExactKeys(asset, [
|
|
87
|
+
"platform",
|
|
88
|
+
"format",
|
|
89
|
+
"source"
|
|
90
|
+
], platform, ["variant"]);
|
|
91
|
+
rejectDuplicateVariants(seenLinuxSvg, variants, "Linux SVG", platform);
|
|
92
|
+
await validateSource(asset.source, "svg", platform);
|
|
93
|
+
recordVariantPlatforms(variantPlatforms, variants, "linux");
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new InvalidAppIconError(`unsupported platform/format pair ${String(assetPlatform)}/${String(format)}`, platform);
|
|
97
|
+
}
|
|
98
|
+
const currentPlatform = normalizePlatform(platform);
|
|
99
|
+
if (currentPlatform !== void 0) {
|
|
100
|
+
if (!variantPlatforms.get("default")?.has(currentPlatform)) throw new InvalidAppIconError(`no ${currentPlatform} ${DEFAULT_APP_ICON_VARIANT} asset was provided`, platform);
|
|
101
|
+
for (const [variant, platforms] of variantPlatforms) if (!platforms.has(currentPlatform)) throw new InvalidAppIconError(`variant ${variant} has no ${currentPlatform} asset`, platform);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/** Normalize encoded sources into JSON array bytes for the Rust wire protocol. */
|
|
105
|
+
const normalizeAppIcon = (icon) => icon.map((asset) => {
|
|
106
|
+
const source = normalizeSource(asset.source);
|
|
107
|
+
const variant = normalizeVariant(asset.variant);
|
|
108
|
+
if (asset.platform === "darwin") return {
|
|
109
|
+
...asset,
|
|
110
|
+
variant,
|
|
111
|
+
source
|
|
112
|
+
};
|
|
113
|
+
if (asset.platform === "windows") return {
|
|
114
|
+
...asset,
|
|
115
|
+
variant,
|
|
116
|
+
source
|
|
117
|
+
};
|
|
118
|
+
if (asset.format === "png") return {
|
|
119
|
+
...asset,
|
|
120
|
+
variant,
|
|
121
|
+
source
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
...asset,
|
|
125
|
+
variant,
|
|
126
|
+
source
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
/** Select one semantic variant while retaining every platform's matching native asset. */
|
|
130
|
+
const selectAppIconVariant = (icon, variant, platform = process.platform) => {
|
|
131
|
+
validateVariantName(variant, platform);
|
|
132
|
+
const selected = icon.filter((asset) => normalizedVariantNames(asset.variant).includes(variant));
|
|
133
|
+
const currentPlatform = normalizePlatform(platform);
|
|
134
|
+
if (selected.length === 0 || currentPlatform !== void 0 && !selected.some((asset) => asset.platform === currentPlatform)) throw new AppIconVariantNotFoundError(variant, platform);
|
|
135
|
+
return selected;
|
|
136
|
+
};
|
|
137
|
+
const normalizeSource = (source) => source.type === "encoded" ? {
|
|
138
|
+
type: "encoded",
|
|
139
|
+
data: Array.from(source.data)
|
|
140
|
+
} : {
|
|
141
|
+
type: "file",
|
|
142
|
+
path: resolve(source.path)
|
|
143
|
+
};
|
|
144
|
+
const normalizeVariant = (variant) => variant === void 0 ? DEFAULT_APP_ICON_VARIANT : typeof variant === "string" ? variant : [...variant];
|
|
145
|
+
const normalizedVariantNames = (variant) => variant === void 0 ? [DEFAULT_APP_ICON_VARIANT] : typeof variant === "string" ? [variant] : variant;
|
|
146
|
+
const validateVariantNames = (variant, platform) => {
|
|
147
|
+
const names = variant === void 0 ? [DEFAULT_APP_ICON_VARIANT] : typeof variant === "string" ? [variant] : Array.isArray(variant) ? variant : [];
|
|
148
|
+
if (names.length === 0) throw new InvalidAppIconError("variant must be a name or non-empty name array", platform);
|
|
149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
150
|
+
for (const name of names) {
|
|
151
|
+
validateVariantName(name, platform);
|
|
152
|
+
if (seen.has(name)) throw new InvalidAppIconError(`duplicate variant name ${name} on one asset`, platform);
|
|
153
|
+
seen.add(name);
|
|
154
|
+
}
|
|
155
|
+
return names;
|
|
156
|
+
};
|
|
157
|
+
function validateVariantName(variant, platform) {
|
|
158
|
+
if (typeof variant !== "string" || !APP_ICON_VARIANT_PATTERN.test(variant)) throw new InvalidAppIconError(`variant must match ${APP_ICON_VARIANT_PATTERN}`, platform);
|
|
159
|
+
}
|
|
160
|
+
const rejectDuplicateVariants = (seen, variants, label, hostPlatform) => {
|
|
161
|
+
for (const variant of variants) {
|
|
162
|
+
if (seen.has(variant)) throw new InvalidAppIconError(`duplicate ${label} asset for variant ${variant}`, hostPlatform);
|
|
163
|
+
seen.add(variant);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const recordVariantPlatforms = (variants, names, platform) => {
|
|
167
|
+
for (const name of names) {
|
|
168
|
+
const platforms = variants.get(name) ?? /* @__PURE__ */ new Set();
|
|
169
|
+
platforms.add(platform);
|
|
170
|
+
variants.set(name, platforms);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const validateSource = async (source, format, platform) => {
|
|
174
|
+
if (!isRecord$1(source)) throw new InvalidAppIconError("asset source must be a file or encoded payload", platform);
|
|
175
|
+
let bytes;
|
|
176
|
+
if (source.type === "file") {
|
|
177
|
+
assertExactKeys(source, ["type", "path"], platform);
|
|
178
|
+
if (typeof source.path !== "string" || source.path.trim().length === 0) throw new InvalidAppIconError("file source path must not be empty", platform);
|
|
179
|
+
try {
|
|
180
|
+
bytes = await readFile(source.path);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new InvalidAppIconError(`unable to read ${source.path}: ${errorMessage(error)}`, platform);
|
|
183
|
+
}
|
|
184
|
+
} else if (source.type === "encoded") {
|
|
185
|
+
assertExactKeys(source, ["type", "data"], platform);
|
|
186
|
+
bytes = encodedBytes(source.data, platform);
|
|
187
|
+
} else throw new InvalidAppIconError("unsupported app icon source type", platform);
|
|
188
|
+
if (!matchesFormat(bytes, format)) throw new InvalidAppIconError(`source bytes do not contain a valid ${format.toUpperCase()} asset`, platform);
|
|
189
|
+
};
|
|
190
|
+
const encodedBytes = (data, platform) => {
|
|
191
|
+
if (data instanceof Uint8Array && data.byteLength > 0) return data;
|
|
192
|
+
if (Array.isArray(data) && data.length > 0 && data.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)) return Uint8Array.from(data);
|
|
193
|
+
throw new InvalidAppIconError("encoded source must contain one or more bytes in the range 0..255", platform);
|
|
194
|
+
};
|
|
195
|
+
const matchesFormat = (bytes, format) => {
|
|
196
|
+
if (format === "icns") return hasPrefix(bytes, [
|
|
197
|
+
105,
|
|
198
|
+
99,
|
|
199
|
+
110,
|
|
200
|
+
115
|
|
201
|
+
]);
|
|
202
|
+
if (format === "ico") return bytes.length >= 6 && hasPrefix(bytes, [
|
|
203
|
+
0,
|
|
204
|
+
0,
|
|
205
|
+
1,
|
|
206
|
+
0
|
|
207
|
+
]) && bytes[4] !== 0;
|
|
208
|
+
if (format === "png") return hasPrefix(bytes, [
|
|
209
|
+
137,
|
|
210
|
+
80,
|
|
211
|
+
78,
|
|
212
|
+
71,
|
|
213
|
+
13,
|
|
214
|
+
10,
|
|
215
|
+
26,
|
|
216
|
+
10
|
|
217
|
+
]);
|
|
218
|
+
const source = new TextDecoder().decode(bytes.subarray(0, 4096)).replace(/^\uFEFF/, "");
|
|
219
|
+
return /<(?:\?xml[^>]*>\s*)?(?:!--[\s\S]*?-->\s*)*svg(?:\s|>)/i.test(source.trimStart());
|
|
220
|
+
};
|
|
221
|
+
const hasPrefix = (bytes, prefix) => bytes.length >= prefix.length && prefix.every((byte, index) => bytes[index] === byte);
|
|
222
|
+
const assertExactKeys = (record, expected, platform, optional = []) => {
|
|
223
|
+
const actual = Object.keys(record);
|
|
224
|
+
const allowed = new Set([...expected, ...optional]);
|
|
225
|
+
const unexpected = actual.find((key) => !allowed.has(key));
|
|
226
|
+
if (unexpected !== void 0) throw new InvalidAppIconError(`unexpected asset field ${unexpected}`, platform);
|
|
227
|
+
const missing = expected.find((key) => !(key in record));
|
|
228
|
+
if (missing !== void 0) throw new InvalidAppIconError(`missing asset field ${missing}`, platform);
|
|
229
|
+
};
|
|
230
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
231
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
232
|
+
const normalizePlatform = (platform) => {
|
|
233
|
+
if (platform === "darwin") return "darwin";
|
|
234
|
+
if (platform === "win32") return "windows";
|
|
235
|
+
if (platform === "linux") return "linux";
|
|
236
|
+
};
|
|
237
|
+
//#endregion
|
|
11
238
|
//#region src/native-extension-artifact.ts
|
|
12
239
|
var NativeExtensionArtifactResolutionError = class extends Error {
|
|
13
240
|
target;
|
|
@@ -136,8 +363,7 @@ function createClient(transport, options = {}) {
|
|
|
136
363
|
const handle = createAppHandle(transport, app.appId, nextRequestId);
|
|
137
364
|
const configured = options.appOptions;
|
|
138
365
|
if (configured?.name !== void 0) await handle.setName(configured.name);
|
|
139
|
-
if (configured?.
|
|
140
|
-
else if (trayOptions.icon !== void 0 && await handle.getIcon() === null) await handle.setIcon(trayOptions.icon);
|
|
366
|
+
if (configured?.appIcon !== void 0) await handle.setAppIcon(configured.appIcon);
|
|
141
367
|
return {
|
|
142
368
|
app,
|
|
143
369
|
handle
|
|
@@ -291,6 +517,11 @@ const attachEventfulTrayHandle = (handle, source, appId, nextMountId) => {
|
|
|
291
517
|
onTrayDoubleClick(handler) {
|
|
292
518
|
return listen("trayDoubleClick", handler);
|
|
293
519
|
},
|
|
520
|
+
onAppReopenRequested(handler) {
|
|
521
|
+
return source.onEvent((frame) => {
|
|
522
|
+
if (frame.type === "app-event" && frame.event.type === "reopenRequested" && frame.event.appId === appId) handler(frame.event);
|
|
523
|
+
});
|
|
524
|
+
},
|
|
294
525
|
extend(extension, options) {
|
|
295
526
|
const context = createTrayExtensionContext(eventful, appId, extension, options, nextMountId);
|
|
296
527
|
const capability = extension.extend(eventful, context, options);
|
|
@@ -332,18 +563,35 @@ const createAppHandle = (transport, appId, nextRequestId) => {
|
|
|
332
563
|
}), requestId, "ack");
|
|
333
564
|
return (await requestIdentity()).appName;
|
|
334
565
|
},
|
|
335
|
-
async
|
|
336
|
-
return (await requestIdentity()).
|
|
566
|
+
async getAppIcon() {
|
|
567
|
+
return (await requestIdentity()).appIcon ?? null;
|
|
337
568
|
},
|
|
338
|
-
async
|
|
569
|
+
async getAppIconVariant() {
|
|
570
|
+
return (await requestIdentity()).appIconVariant ?? null;
|
|
571
|
+
},
|
|
572
|
+
async setAppIcon(iconOrVariant) {
|
|
573
|
+
if (typeof iconOrVariant === "string") {
|
|
574
|
+
const identity = await requestIdentity();
|
|
575
|
+
if (identity.appIcon === void 0) selectAppIconVariant([], iconOrVariant);
|
|
576
|
+
else selectAppIconVariant(identity.appIcon, iconOrVariant);
|
|
577
|
+
const requestId = nextRequestId();
|
|
578
|
+
expectResponse(await transport.request({
|
|
579
|
+
type: "set-app-icon-variant",
|
|
580
|
+
requestId,
|
|
581
|
+
appId,
|
|
582
|
+
variant: iconOrVariant
|
|
583
|
+
}), requestId, "ack");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (iconOrVariant !== null) await validateAppIcon(iconOrVariant);
|
|
587
|
+
const appIcon = iconOrVariant === null ? null : normalizeAppIcon(iconOrVariant);
|
|
339
588
|
const requestId = nextRequestId();
|
|
340
589
|
expectResponse(await transport.request({
|
|
341
590
|
type: "set-app-icon",
|
|
342
591
|
requestId,
|
|
343
592
|
appId,
|
|
344
|
-
|
|
593
|
+
appIcon
|
|
345
594
|
}), requestId, "ack");
|
|
346
|
-
return icon;
|
|
347
595
|
}
|
|
348
596
|
};
|
|
349
597
|
};
|
|
@@ -412,6 +660,7 @@ const requestIdOf = (frame) => {
|
|
|
412
660
|
case "error": return frame.requestId;
|
|
413
661
|
case "ready":
|
|
414
662
|
case "event":
|
|
663
|
+
case "app-event":
|
|
415
664
|
case "ext-event": return;
|
|
416
665
|
}
|
|
417
666
|
};
|
|
@@ -444,7 +693,8 @@ const resolveBrokerNativeTarget = (platform = process.platform, arch = process.a
|
|
|
444
693
|
const packageOs = platformToPackageOs(platform);
|
|
445
694
|
return {
|
|
446
695
|
packageName: `@opentray/${packageOs}-${normalizeArch(arch)}`,
|
|
447
|
-
binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}
|
|
696
|
+
binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`,
|
|
697
|
+
...packageOs === "darwin" ? { carrierTemplateRelativePath: "app/Info.plist" } : {}
|
|
448
698
|
};
|
|
449
699
|
};
|
|
450
700
|
const resolveBrokerArtifactTarget = (platform = process.platform, arch = process.arch) => {
|
|
@@ -520,10 +770,19 @@ const resolveBrokerCommand = async (paths, options = {}) => {
|
|
|
520
770
|
const arch = options.arch ?? process.arch;
|
|
521
771
|
const target = resolveBrokerNativeTarget(platform, arch);
|
|
522
772
|
const installed = await (options.resolveInstalledBrokerBinary ?? resolveInstalledBrokerBinary)(target, { platform });
|
|
523
|
-
if (installed.binary !== void 0)
|
|
773
|
+
if (installed.binary !== void 0) {
|
|
774
|
+
if (platform !== "darwin") return commandForBinary(installed.binary, paths);
|
|
775
|
+
if (options.appBundle === void 0) return commandForBinary(installed.binary, paths);
|
|
776
|
+
if (installed.carrierTemplate === void 0) throw missingDarwinCarrierError(target, installed, arch);
|
|
777
|
+
return commandForBinary(await ensureDarwinBundle(options, paths, installed.binary, installed.carrierTemplate), paths);
|
|
778
|
+
}
|
|
524
779
|
const sourceDir = options.sourceDir ?? dirname(fileURLToPath(sourceUrl));
|
|
525
780
|
const workspaceRoot = await (options.findWorkspaceRoot ?? findWorkspaceRoot)(sourceDir);
|
|
526
|
-
if (workspaceRoot !== void 0)
|
|
781
|
+
if (workspaceRoot !== void 0) {
|
|
782
|
+
const binary = await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot, paths, platform);
|
|
783
|
+
if (platform !== "darwin") return commandForBinary(binary, paths);
|
|
784
|
+
return commandForBinary(options.appBundle === void 0 ? binary : await ensureDarwinBundle(options, paths, binary, await (options.ensureDevDarwinCarrierTemplate ?? ensureDevDarwinCarrierTemplate)(workspaceRoot, paths)), paths);
|
|
785
|
+
}
|
|
527
786
|
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, {
|
|
528
787
|
packageName: target.packageName,
|
|
529
788
|
...installed.binaryPath === void 0 ? {} : { binaryPath: installed.binaryPath }
|
|
@@ -539,14 +798,28 @@ const resolveInstalledBrokerBinary = async (target, options = {}) => {
|
|
|
539
798
|
throw error;
|
|
540
799
|
}
|
|
541
800
|
const binaryPath = join(dirname(packageJsonPath), target.binaryRelativePath);
|
|
801
|
+
const carrierTemplatePath = target.carrierTemplateRelativePath === void 0 ? void 0 : join(dirname(packageJsonPath), target.carrierTemplateRelativePath);
|
|
802
|
+
let carrierTemplate;
|
|
803
|
+
if (carrierTemplatePath !== void 0) try {
|
|
804
|
+
await (options.assertCarrierAccessible ?? assertInstalledCarrierAccessible)(carrierTemplatePath);
|
|
805
|
+
carrierTemplate = carrierTemplatePath;
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (!isNodeError$1(error) || error.code !== "ENOENT" && error.code !== "EACCES" && error.code !== "EPERM") throw error;
|
|
808
|
+
}
|
|
542
809
|
try {
|
|
543
810
|
await (options.assertBinaryAccessible ?? assertInstalledBrokerBinaryAccessible)(binaryPath, platform);
|
|
544
811
|
return {
|
|
545
812
|
binary: binaryPath,
|
|
546
|
-
binaryPath
|
|
813
|
+
binaryPath,
|
|
814
|
+
...carrierTemplatePath === void 0 ? {} : { carrierTemplatePath },
|
|
815
|
+
...carrierTemplate === void 0 ? {} : { carrierTemplate }
|
|
547
816
|
};
|
|
548
817
|
} catch (error) {
|
|
549
|
-
if (isNodeError$1(error) && (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM")) return {
|
|
818
|
+
if (isNodeError$1(error) && (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM")) return {
|
|
819
|
+
binaryPath,
|
|
820
|
+
...carrierTemplatePath === void 0 ? {} : { carrierTemplatePath },
|
|
821
|
+
...carrierTemplate === void 0 ? {} : { carrierTemplate }
|
|
822
|
+
};
|
|
550
823
|
throw error;
|
|
551
824
|
}
|
|
552
825
|
};
|
|
@@ -593,15 +866,39 @@ const ensureDevBrokerBinary = async (workspaceRoot, paths, platform) => {
|
|
|
593
866
|
await runCargoBuild(workspaceRoot, platform === "win32" ? targetDir : void 0);
|
|
594
867
|
return binary;
|
|
595
868
|
};
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
869
|
+
const ensureDevDarwinCarrierTemplate = async (workspaceRoot, _paths) => {
|
|
870
|
+
return join(workspaceRoot, "packages/darwin-app-carrier/Info.plist");
|
|
871
|
+
};
|
|
872
|
+
const ensureDarwinBundle = async (options, paths, brokerPath, templatePath) => {
|
|
873
|
+
const appBundle = options.appBundle;
|
|
874
|
+
if (appBundle === void 0) return brokerPath;
|
|
875
|
+
const arch = options.arch ?? process.arch;
|
|
876
|
+
if (arch !== "arm64" && arch !== "x64") throw new Error(`unsupported Darwin app bundle architecture: ${arch}`);
|
|
877
|
+
return ensureDarwinAppBundle({
|
|
878
|
+
bundlePath: appBundle.path,
|
|
879
|
+
packageName: options.packageIdentity?.name ?? paths.callerLabel,
|
|
880
|
+
appId: paths.appId,
|
|
881
|
+
appName: paths.appName,
|
|
882
|
+
target: {
|
|
883
|
+
os: "darwin",
|
|
884
|
+
arch
|
|
885
|
+
},
|
|
886
|
+
brokerPath,
|
|
887
|
+
templatePath,
|
|
888
|
+
...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
|
|
889
|
+
...appBundle.reinitialize === void 0 ? {} : { reinitialize: appBundle.reinitialize }
|
|
890
|
+
});
|
|
891
|
+
};
|
|
892
|
+
const runCargoBuild = (workspaceRoot, targetDir) => runProcess("cargo", [
|
|
893
|
+
"build",
|
|
894
|
+
"-p",
|
|
895
|
+
"opentray-bin",
|
|
896
|
+
...targetDir === void 0 ? [] : ["--target-dir", targetDir]
|
|
897
|
+
], workspaceRoot, process.env.OPENTRAY_BROKER_BUILD_LOGS === "1" ? "inherit" : "ignore");
|
|
898
|
+
const runProcess = (command, args, cwd, stdio = "ignore") => new Promise((resolve, reject) => {
|
|
899
|
+
const child = spawn(command, [...args], {
|
|
900
|
+
cwd,
|
|
901
|
+
stdio
|
|
605
902
|
});
|
|
606
903
|
child.once("error", reject);
|
|
607
904
|
child.once("exit", (code) => {
|
|
@@ -609,7 +906,7 @@ const runCargoBuild = (workspaceRoot, targetDir) => new Promise((resolve, reject
|
|
|
609
906
|
resolve();
|
|
610
907
|
return;
|
|
611
908
|
}
|
|
612
|
-
reject(/* @__PURE__ */ new Error(
|
|
909
|
+
reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} failed with code ${code ?? "unknown"}`));
|
|
613
910
|
});
|
|
614
911
|
});
|
|
615
912
|
const exists = async (path) => {
|
|
@@ -624,52 +921,134 @@ const binaryAccessMode = (platform) => platform === "win32" ? constants.F_OK : c
|
|
|
624
921
|
const assertInstalledBrokerBinaryAccessible = async (binaryPath, platform) => {
|
|
625
922
|
await access(binaryPath, binaryAccessMode(platform));
|
|
626
923
|
};
|
|
924
|
+
const assertInstalledCarrierAccessible = async (carrierPath) => {
|
|
925
|
+
await access(carrierPath, constants.F_OK);
|
|
926
|
+
};
|
|
927
|
+
const missingDarwinCarrierError = (target, resolution, arch) => new MissingPlatformBrokerBinaryError(`unable to resolve OpenTray Darwin app bundle template for ${target.packageName}; expected ${resolution.carrierTemplatePath ?? target.carrierTemplateRelativePath ?? "app/Info.plist"}`, "darwin", arch, {
|
|
928
|
+
packageName: target.packageName,
|
|
929
|
+
...resolution.carrierTemplatePath === void 0 ? {} : { binaryPath: resolution.carrierTemplatePath }
|
|
930
|
+
});
|
|
627
931
|
function isNodeError$1(error) {
|
|
628
932
|
return error instanceof Error && "code" in error;
|
|
629
933
|
}
|
|
630
934
|
//#endregion
|
|
631
935
|
//#region src/daemon/lifecycle.ts
|
|
632
936
|
const DAEMON_STDIO_ENV = "OPENTRAY_DAEMON_STDIO";
|
|
633
|
-
const
|
|
634
|
-
const
|
|
937
|
+
const DAEMON_POLL_INTERVAL_MS = 50;
|
|
938
|
+
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 1e4;
|
|
939
|
+
const DEFAULT_DAEMON_LOCK_TIMEOUT_MS = 15e3;
|
|
940
|
+
const ABANDONED_RECLAIM_CLAIM_MS = 1e3;
|
|
941
|
+
const createNodeDaemonDriver = (cliEntrypoint, options = {}) => ({
|
|
635
942
|
async resolveBroker(paths) {
|
|
636
|
-
return resolveBrokerArtifact(paths
|
|
943
|
+
return resolveBrokerArtifact(paths, {
|
|
944
|
+
...options.appBundle === void 0 ? {} : { appBundle: options.appBundle },
|
|
945
|
+
...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
|
|
946
|
+
...options.packageIdentity === void 0 ? {} : { packageIdentity: options.packageIdentity }
|
|
947
|
+
});
|
|
637
948
|
},
|
|
638
949
|
async isAlive(pid) {
|
|
639
950
|
return isProcessAlive(pid);
|
|
640
951
|
},
|
|
641
952
|
async spawnBroker(paths, broker) {
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
953
|
+
const env = options.env ?? process.env;
|
|
954
|
+
const stdio = resolveBrokerStdio(env[DAEMON_STDIO_ENV]);
|
|
955
|
+
if (stdio === "log") await mkdir(paths.runtimeDir, { recursive: true });
|
|
956
|
+
const logHandle = stdio === "log" ? await open(paths.brokerLog, "a") : void 0;
|
|
957
|
+
if (logHandle !== void 0) await logHandle.appendFile(`${JSON.stringify({
|
|
958
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
959
|
+
event: "broker-spawn",
|
|
960
|
+
command: broker.command,
|
|
961
|
+
cwd: broker.cwd ?? process.cwd(),
|
|
962
|
+
appId: paths.appId,
|
|
963
|
+
appName: paths.appName
|
|
964
|
+
})}\n`, "utf8");
|
|
965
|
+
if (isAbsolute(broker.command)) try {
|
|
966
|
+
await access(broker.command, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
if (stdio === "log") await appendFile(paths.brokerLog, `${JSON.stringify({
|
|
969
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
970
|
+
event: "broker-spawn-error",
|
|
971
|
+
command: broker.command,
|
|
972
|
+
error: error instanceof Error ? error.message : String(error)
|
|
973
|
+
})}\n`, "utf8").catch(() => void 0);
|
|
974
|
+
await logHandle?.close();
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
let child;
|
|
978
|
+
try {
|
|
979
|
+
child = spawn(broker.command, broker.args, {
|
|
980
|
+
cwd: broker.cwd,
|
|
981
|
+
detached: true,
|
|
982
|
+
env: {
|
|
983
|
+
...env,
|
|
984
|
+
OPENTRAY_DAEMON_HOME: paths.homeDir,
|
|
985
|
+
OPENTRAY_DAEMON_PACKAGE_VERSION: paths.packageVersion,
|
|
986
|
+
OPENTRAY_DAEMON_CLI_ENTRYPOINT: cliEntrypoint,
|
|
987
|
+
OPENTRAY_DAEMON_APP_ID: paths.appId,
|
|
988
|
+
OPENTRAY_DAEMON_APP_NAME: paths.appName,
|
|
989
|
+
OPENTRAY_DAEMON_CALLER_LABEL: paths.callerLabel
|
|
990
|
+
},
|
|
991
|
+
stdio: logHandle === void 0 ? stdio === "log" ? "ignore" : stdio : [
|
|
992
|
+
"ignore",
|
|
993
|
+
logHandle.fd,
|
|
994
|
+
logHandle.fd
|
|
995
|
+
]
|
|
996
|
+
});
|
|
997
|
+
} finally {
|
|
998
|
+
await logHandle?.close();
|
|
999
|
+
}
|
|
1000
|
+
child.on("error", (error) => {
|
|
1001
|
+
if (stdio !== "log") return;
|
|
1002
|
+
appendFile(paths.brokerLog, `${JSON.stringify({
|
|
1003
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1004
|
+
event: "broker-spawn-error",
|
|
1005
|
+
command: broker.command,
|
|
1006
|
+
error: error.message
|
|
1007
|
+
})}\n`, "utf8").catch(() => void 0);
|
|
1008
|
+
});
|
|
1009
|
+
child.once("exit", (code, signal) => {
|
|
1010
|
+
if (stdio !== "log") return;
|
|
1011
|
+
appendFile(paths.brokerLog, `${JSON.stringify({
|
|
1012
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1013
|
+
event: "broker-exit",
|
|
1014
|
+
command: broker.command,
|
|
1015
|
+
code,
|
|
1016
|
+
signal
|
|
1017
|
+
})}\n`, "utf8").catch(() => void 0);
|
|
655
1018
|
});
|
|
656
1019
|
child.unref();
|
|
657
|
-
|
|
1020
|
+
const pid = child.pid ?? 0;
|
|
1021
|
+
if (process.platform === "darwin" && options.appBundle !== void 0 && pid > 0) await writeDarwinAppBundleOwner(options.appBundle.path, pid, broker.artifactIdentity.executableHash);
|
|
1022
|
+
return pid;
|
|
658
1023
|
},
|
|
659
1024
|
async stop(pid) {
|
|
660
1025
|
process.kill(pid, "SIGTERM");
|
|
1026
|
+
if (process.platform === "darwin" && options.appBundle !== void 0) await clearDarwinAppBundleOwner(options.appBundle.path, pid);
|
|
661
1027
|
}
|
|
662
1028
|
});
|
|
663
1029
|
const resolveBrokerStdio = (value) => {
|
|
664
|
-
|
|
1030
|
+
if (value === "inherit") return "inherit";
|
|
1031
|
+
if (value === "ignore") return "ignore";
|
|
1032
|
+
return "log";
|
|
665
1033
|
};
|
|
666
|
-
const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS }) => {
|
|
667
|
-
const broker = await driver.resolveBroker(paths);
|
|
1034
|
+
const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS, readinessTimeoutMs = DEFAULT_DAEMON_READY_TIMEOUT_MS }) => {
|
|
668
1035
|
await mkdir(paths.runtimeDir, { recursive: true });
|
|
669
|
-
const lock = await
|
|
1036
|
+
const lock = await acquireDaemonLock(paths.lockFile, lockTimeoutMs);
|
|
670
1037
|
try {
|
|
671
1038
|
const existingPid = await readPid(paths.pidFile);
|
|
672
|
-
|
|
1039
|
+
let existingAlive = existingPid !== void 0 && await driver.isAlive(existingPid);
|
|
1040
|
+
let broker;
|
|
1041
|
+
try {
|
|
1042
|
+
broker = await driver.resolveBroker(paths);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (existingAlive && isBundleInUseError(error) && existingPid !== void 0) {
|
|
1045
|
+
await driver.stop(existingPid);
|
|
1046
|
+
if (!await waitUntilStopped(driver, existingPid)) throw new Error(`incompatible daemon broker did not stop within the bounded shutdown window: pid=${existingPid}`);
|
|
1047
|
+
existingAlive = false;
|
|
1048
|
+
broker = await driver.resolveBroker(paths);
|
|
1049
|
+
} else throw error;
|
|
1050
|
+
}
|
|
1051
|
+
if (existingPid !== void 0 && existingAlive) {
|
|
673
1052
|
if (readyMatchesBroker(await readReadyMetadata(paths.readyFile), paths, existingPid, broker)) return {
|
|
674
1053
|
status: "already-running",
|
|
675
1054
|
pid: existingPid,
|
|
@@ -684,7 +1063,7 @@ const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_
|
|
|
684
1063
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("daemon broker process did not expose a valid pid");
|
|
685
1064
|
await writeFile(paths.pidFile, `${pid}\n`, "utf8");
|
|
686
1065
|
try {
|
|
687
|
-
await waitForReadyFile(paths, driver, pid, broker);
|
|
1066
|
+
await waitForReadyFile(paths, driver, pid, broker, readinessTimeoutMs);
|
|
688
1067
|
} catch (error) {
|
|
689
1068
|
if (await driver.isAlive(pid)) {
|
|
690
1069
|
await driver.stop(pid);
|
|
@@ -721,19 +1100,22 @@ const cleanupRuntimeFiles = async (paths) => {
|
|
|
721
1100
|
const waitUntilStopped = async (driver, pid) => {
|
|
722
1101
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
723
1102
|
if (!await driver.isAlive(pid)) return true;
|
|
724
|
-
await sleep(
|
|
1103
|
+
await sleep(DAEMON_POLL_INTERVAL_MS);
|
|
725
1104
|
}
|
|
726
1105
|
return false;
|
|
727
1106
|
};
|
|
728
|
-
const waitForReadyFile = async (paths, driver, pid, broker) => {
|
|
729
|
-
|
|
1107
|
+
const waitForReadyFile = async (paths, driver, pid, broker, timeoutMs) => {
|
|
1108
|
+
const deadline = Date.now() + timeoutMs;
|
|
1109
|
+
while (true) {
|
|
730
1110
|
const ready = await readReadyMetadata(paths.readyFile);
|
|
731
1111
|
if (readyMatchesBroker(ready, paths, pid, broker)) return;
|
|
732
1112
|
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
|
|
734
|
-
|
|
1113
|
+
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`);
|
|
1114
|
+
const remainingMs = deadline - Date.now();
|
|
1115
|
+
if (remainingMs <= 0) break;
|
|
1116
|
+
await sleep(Math.min(DAEMON_POLL_INTERVAL_MS, remainingMs));
|
|
735
1117
|
}
|
|
736
|
-
throw new Error(`timed out waiting for daemon readiness
|
|
1118
|
+
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`);
|
|
737
1119
|
};
|
|
738
1120
|
const readReadyMetadata = async (readyFile) => {
|
|
739
1121
|
let value;
|
|
@@ -758,24 +1140,31 @@ const readReadyMetadata = async (readyFile) => {
|
|
|
758
1140
|
};
|
|
759
1141
|
};
|
|
760
1142
|
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);
|
|
1143
|
+
const isBundleInUseError = (error) => error instanceof Error && "code" in error && error.code === "bundle_in_use";
|
|
761
1144
|
const executablePathsEqual = (left, right, platform = process.platform) => {
|
|
762
1145
|
if (platform !== "win32") return left === right;
|
|
763
1146
|
const normalize = (value) => value.replaceAll("\\", "/").replace(/^\/\/?\?\//u, "").toLowerCase();
|
|
764
1147
|
return normalize(left) === normalize(right);
|
|
765
1148
|
};
|
|
766
|
-
const
|
|
1149
|
+
const acquireDaemonLock = async (lockFile, timeoutMs) => {
|
|
767
1150
|
const deadline = Date.now() + timeoutMs;
|
|
1151
|
+
const owner = `${JSON.stringify({
|
|
1152
|
+
pid: process.pid,
|
|
1153
|
+
token: randomUUID()
|
|
1154
|
+
})}\n`;
|
|
768
1155
|
while (Date.now() <= deadline) try {
|
|
769
1156
|
const handle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
770
|
-
await handle.writeFile(
|
|
1157
|
+
await handle.writeFile(owner, "utf8");
|
|
771
1158
|
return { async release() {
|
|
772
1159
|
await handle.close();
|
|
1160
|
+
if (await readLockSource(lockFile) !== owner) return;
|
|
773
1161
|
await unlink(lockFile).catch((error) => {
|
|
774
1162
|
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
775
1163
|
});
|
|
776
1164
|
} };
|
|
777
1165
|
} catch (error) {
|
|
778
1166
|
if (isNodeError(error) && error.code === "EEXIST") {
|
|
1167
|
+
if (await reclaimDeadLockOwner(lockFile)) continue;
|
|
779
1168
|
await sleep(25);
|
|
780
1169
|
continue;
|
|
781
1170
|
}
|
|
@@ -783,12 +1172,72 @@ const acquireLock = async (lockFile, timeoutMs) => {
|
|
|
783
1172
|
}
|
|
784
1173
|
throw new Error(`timed out acquiring daemon lock: ${lockFile}`);
|
|
785
1174
|
};
|
|
1175
|
+
const reclaimDeadLockOwner = async (lockFile) => {
|
|
1176
|
+
const observed = await readLockSource(lockFile);
|
|
1177
|
+
if (observed === void 0) return true;
|
|
1178
|
+
const owner = parseLockOwner(observed);
|
|
1179
|
+
if (owner === void 0 || isProcessAlive(owner.pid)) return false;
|
|
1180
|
+
const claimFile = `${lockFile}.reclaim-${createHash("sha256").update(observed).digest("hex").slice(0, 16)}`;
|
|
1181
|
+
try {
|
|
1182
|
+
await link(lockFile, claimFile);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (isNodeError(error) && error.code === "ENOENT") return true;
|
|
1185
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
1186
|
+
await clearAbandonedReclaimClaim(claimFile);
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1191
|
+
try {
|
|
1192
|
+
if (await readLockSource(lockFile) !== observed || isProcessAlive(owner.pid)) return false;
|
|
1193
|
+
try {
|
|
1194
|
+
await unlink(lockFile);
|
|
1195
|
+
return true;
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
if (isNodeError(error) && error.code === "ENOENT") return true;
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
} finally {
|
|
1201
|
+
await unlink(claimFile).catch((error) => {
|
|
1202
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
const clearAbandonedReclaimClaim = async (claimFile) => {
|
|
1207
|
+
try {
|
|
1208
|
+
const claim = await stat(claimFile);
|
|
1209
|
+
if (Date.now() - claim.ctimeMs < ABANDONED_RECLAIM_CLAIM_MS) return;
|
|
1210
|
+
await unlink(claimFile);
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
const readLockSource = async (lockFile) => {
|
|
1216
|
+
try {
|
|
1217
|
+
return await readFile(lockFile, "utf8");
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
1220
|
+
throw error;
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const parseLockOwner = (source) => {
|
|
1224
|
+
const legacyPid = Number(source.trim());
|
|
1225
|
+
if (Number.isInteger(legacyPid) && legacyPid > 0) return { pid: legacyPid };
|
|
1226
|
+
let value;
|
|
1227
|
+
try {
|
|
1228
|
+
value = JSON.parse(source);
|
|
1229
|
+
} catch {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (!isRecord(value) || typeof value.pid !== "number" || !Number.isInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || value.token.length === 0) return;
|
|
1233
|
+
return { pid: value.pid };
|
|
1234
|
+
};
|
|
786
1235
|
const isProcessAlive = (pid) => {
|
|
787
1236
|
try {
|
|
788
1237
|
process.kill(pid, 0);
|
|
789
1238
|
return true;
|
|
790
|
-
} catch {
|
|
791
|
-
return
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
return !(isNodeError(error) && error.code === "ESRCH");
|
|
792
1241
|
}
|
|
793
1242
|
};
|
|
794
1243
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -827,7 +1276,8 @@ const resolveDaemonPaths = ({ homeDir, packageVersion, callerLabel, appId, appNa
|
|
|
827
1276
|
endpoint,
|
|
828
1277
|
pidFile: `${runtimeDir}/broker.pid`,
|
|
829
1278
|
lockFile: `${runtimeDir}/broker.lock`,
|
|
830
|
-
readyFile: `${runtimeDir}/ready.json
|
|
1279
|
+
readyFile: `${runtimeDir}/ready.json`,
|
|
1280
|
+
brokerLog: `${runtimeDir}/broker.log`
|
|
831
1281
|
};
|
|
832
1282
|
};
|
|
833
1283
|
const normalizeAppIdentityField$1 = (value, fallback) => {
|
|
@@ -854,7 +1304,16 @@ const connectLocalBroker = async (options = {}) => {
|
|
|
854
1304
|
const autoStart = options.autoStart ?? endpoint === paths.endpoint;
|
|
855
1305
|
if (autoStart && endpoint !== paths.endpoint) throw new Error("local broker autoStart requires the derived same-version endpoint");
|
|
856
1306
|
const cliEntrypoint = options.cliEntrypoint ?? resolveCliEntrypoint();
|
|
857
|
-
const
|
|
1307
|
+
const packageIdentity = process.platform === "darwin" && autoStart ? await resolveOpenTrayPackageIdentity({
|
|
1308
|
+
...options.packageName === void 0 ? {} : { packageName: options.packageName },
|
|
1309
|
+
...options.packageRoot === void 0 ? {} : { packageRoot: options.packageRoot }
|
|
1310
|
+
}) : void 0;
|
|
1311
|
+
const appBundle = process.platform === "darwin" && autoStart ? resolveDarwinAppBundleOptions(options.appBundle, paths, packageIdentity) : void 0;
|
|
1312
|
+
const driver = options.daemonDriver ?? createNodeDaemonDriver(cliEntrypoint, {
|
|
1313
|
+
...appBundle === void 0 ? {} : { appBundle },
|
|
1314
|
+
...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
|
|
1315
|
+
...packageIdentity === void 0 ? {} : { packageIdentity }
|
|
1316
|
+
});
|
|
858
1317
|
const expectedBrokerArtifactIdentity = autoStart ? (await startDaemon({
|
|
859
1318
|
paths,
|
|
860
1319
|
driver
|
|
@@ -862,12 +1321,51 @@ const connectLocalBroker = async (options = {}) => {
|
|
|
862
1321
|
const connection = new LocalBrokerConnection(await connectSocket(endpoint), endpoint, callerLabel);
|
|
863
1322
|
try {
|
|
864
1323
|
await connection.init(clientVersion, options.protocolVersion ?? PROTOCOL_VERSION$1, expectedBrokerArtifactIdentity);
|
|
1324
|
+
if (appBundle !== void 0 && options.appLaunch !== void 0) {
|
|
1325
|
+
await updateDarwinAppLaunchDescriptor(appBundle.path, options.appLaunch);
|
|
1326
|
+
const convergence = await convergeDarwinAppBundleIdentity({
|
|
1327
|
+
currentBundlePath: appBundle.path,
|
|
1328
|
+
appId: paths.appId,
|
|
1329
|
+
managedAppsRoot: resolve(paths.homeDir, ".opentray/apps"),
|
|
1330
|
+
legacyRuntimeRoot: resolve(paths.homeDir, ".opentray"),
|
|
1331
|
+
legacyBundlePaths: [resolve(paths.runtimeDir, "darwin-carrier/OpenTray.app")]
|
|
1332
|
+
});
|
|
1333
|
+
await appendBrokerDiagnostic(paths.brokerLog, {
|
|
1334
|
+
event: "bundle-identity-convergence",
|
|
1335
|
+
removed: convergence.removed,
|
|
1336
|
+
skippedLiveOwner: convergence.skippedLiveOwner,
|
|
1337
|
+
failedUnregister: convergence.failedUnregister
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
865
1340
|
} catch (error) {
|
|
866
1341
|
await connection.close();
|
|
867
1342
|
throw error;
|
|
868
1343
|
}
|
|
869
1344
|
return connection;
|
|
870
1345
|
};
|
|
1346
|
+
const appendBrokerDiagnostic = async (path, value) => {
|
|
1347
|
+
try {
|
|
1348
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1349
|
+
await appendFile(path, `${JSON.stringify({
|
|
1350
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1351
|
+
...value
|
|
1352
|
+
})}\n`, "utf8");
|
|
1353
|
+
} catch {}
|
|
1354
|
+
};
|
|
1355
|
+
const resolveDarwinAppBundleOptions = (configured, paths, packageIdentity) => {
|
|
1356
|
+
if (packageIdentity === void 0) throw new Error("Darwin app bundle resolution requires the caller package identity");
|
|
1357
|
+
const configuredPath = configured?.path;
|
|
1358
|
+
return {
|
|
1359
|
+
path: configuredPath === void 0 ? resolveDefaultDarwinAppBundlePath({
|
|
1360
|
+
homeDir: paths.homeDir,
|
|
1361
|
+
packageName: packageIdentity.name,
|
|
1362
|
+
appName: paths.appName
|
|
1363
|
+
}) : typeof configuredPath === "string" ? isAbsolute(configuredPath) ? configuredPath : resolve(packageIdentity.root, configuredPath) : configuredPath.protocol === "file:" ? fileURLToPath(configuredPath) : (() => {
|
|
1364
|
+
throw new Error(`Darwin appBundle.path must be a file URL: ${configuredPath.href}`);
|
|
1365
|
+
})(),
|
|
1366
|
+
...configured?.reinitialize === void 0 ? {} : { reinitialize: configured.reinitialize }
|
|
1367
|
+
};
|
|
1368
|
+
};
|
|
871
1369
|
const normalizeAppIdentityField = (value) => {
|
|
872
1370
|
const trimmed = value?.trim();
|
|
873
1371
|
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
@@ -980,7 +1478,7 @@ var LocalBrokerConnection = class {
|
|
|
980
1478
|
this.pending.delete(requestId);
|
|
981
1479
|
return;
|
|
982
1480
|
}
|
|
983
|
-
if (frame.type === "event" || frame.type === "ext-event") for (const listener of this.listeners) listener(frame);
|
|
1481
|
+
if (frame.type === "event" || frame.type === "app-event" || frame.type === "ext-event") for (const listener of this.listeners) listener(frame);
|
|
984
1482
|
}
|
|
985
1483
|
write(frame) {
|
|
986
1484
|
this.socket.write(`${JSON.stringify(frame)}\n`);
|
|
@@ -1012,6 +1510,7 @@ const responseRequestId = (frame) => {
|
|
|
1012
1510
|
case "runtime-host-health": return frame.requestId;
|
|
1013
1511
|
case "ready":
|
|
1014
1512
|
case "event":
|
|
1513
|
+
case "app-event":
|
|
1015
1514
|
case "ext-event":
|
|
1016
1515
|
case "error": return;
|
|
1017
1516
|
}
|
|
@@ -1139,14 +1638,53 @@ const registerClickId = (state, id) => {
|
|
|
1139
1638
|
state.seenClickIds = seen;
|
|
1140
1639
|
};
|
|
1141
1640
|
//#endregion
|
|
1641
|
+
//#region src/app-launch.ts
|
|
1642
|
+
const normalizeAppLaunch = (configured, context = {}) => {
|
|
1643
|
+
const currentCwd = context.cwd ?? process.cwd();
|
|
1644
|
+
if (configured === void 0 || configured === null) return {
|
|
1645
|
+
schemaVersion: 1,
|
|
1646
|
+
command: validateRequiredText(context.execPath ?? process.execPath, "process.execPath"),
|
|
1647
|
+
args: [...(context.argv ?? process.argv).slice(1)].map((arg, index) => validateArg(arg, `process.argv[${index + 1}]`)),
|
|
1648
|
+
cwd: validateRequiredText(currentCwd, "process.cwd()")
|
|
1649
|
+
};
|
|
1650
|
+
const command = validateRequiredText(configured.command, "appLaunch.command");
|
|
1651
|
+
const cwd = resolve(currentCwd, configured.cwd === void 0 ? currentCwd : validateRequiredText(configured.cwd, "appLaunch.cwd"));
|
|
1652
|
+
const args = [...configured.args ?? []].map((arg, index) => validateArg(arg, `appLaunch.args[${index}]`));
|
|
1653
|
+
return {
|
|
1654
|
+
schemaVersion: 1,
|
|
1655
|
+
command: resolvePathLikeCommand(command, currentCwd),
|
|
1656
|
+
args,
|
|
1657
|
+
cwd
|
|
1658
|
+
};
|
|
1659
|
+
};
|
|
1660
|
+
const resolvePathLikeCommand = (command, cwd) => {
|
|
1661
|
+
if (isAbsolute(command) || command.includes("/") || command.includes("\\")) return resolve(cwd, command);
|
|
1662
|
+
return command;
|
|
1663
|
+
};
|
|
1664
|
+
const validateRequiredText = (value, field) => {
|
|
1665
|
+
if (value.trim().length === 0 || value.includes("\0")) throw new Error(`${field} must be a non-empty string without NUL`);
|
|
1666
|
+
return value;
|
|
1667
|
+
};
|
|
1668
|
+
const validateArg = (value, field) => {
|
|
1669
|
+
if (value.includes("\0")) throw new Error(`${field} must be a string without NUL`);
|
|
1670
|
+
return value;
|
|
1671
|
+
};
|
|
1672
|
+
//#endregion
|
|
1142
1673
|
//#region src/sdk.ts
|
|
1143
1674
|
const createTray = async (options, runtimeOptions = {}) => {
|
|
1675
|
+
if (runtimeOptions.appIcon !== void 0) await validateAppIcon(runtimeOptions.appIcon);
|
|
1676
|
+
const appIcon = runtimeOptions.appIcon === void 0 ? void 0 : normalizeAppIcon(runtimeOptions.appIcon);
|
|
1144
1677
|
const normalized = normalizeCreateTrayOptions(options);
|
|
1145
|
-
const
|
|
1678
|
+
const appLaunch = normalizeAppLaunch(runtimeOptions.appLaunch);
|
|
1679
|
+
const connection = await connectLocalBroker({
|
|
1680
|
+
...runtimeOptions,
|
|
1681
|
+
...appIcon === void 0 ? {} : { appIcon },
|
|
1682
|
+
appLaunch
|
|
1683
|
+
});
|
|
1146
1684
|
try {
|
|
1147
1685
|
const tray = await createClient(connection, { appOptions: {
|
|
1148
1686
|
...runtimeOptions.appName === void 0 ? {} : { name: runtimeOptions.appName },
|
|
1149
|
-
...
|
|
1687
|
+
...appIcon === void 0 ? {} : { appIcon }
|
|
1150
1688
|
} }).createTray(normalized.options);
|
|
1151
1689
|
return wrapCreateTrayHandle(tray, {
|
|
1152
1690
|
closeConnection: () => connection.close(),
|
|
@@ -1211,6 +1749,6 @@ const bindMenuClickHandlers = (tray, handlers) => {
|
|
|
1211
1749
|
};
|
|
1212
1750
|
const noop = () => {};
|
|
1213
1751
|
//#endregion
|
|
1214
|
-
export { NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact };
|
|
1752
|
+
export { AppIconVariantNotFoundError, DEFAULT_APP_ICON_VARIANT, InvalidAppIconError, NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact, validateAppIcon };
|
|
1215
1753
|
|
|
1216
1754
|
//# sourceMappingURL=index.mjs.map
|