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/README.md +9 -2
- package/dist/index.d.mts +68 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +340 -94
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -9
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 =
|
|
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))
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
|
536
|
-
for (let attempt = 0; attempt <
|
|
537
|
-
await
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
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
|
|
920
|
-
|
|
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.
|
|
955
|
-
|
|
956
|
-
|
|
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
|