@nowcrew/daemon 0.6.58 → 0.6.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/computer-cli.js +10 -1
- package/dist/computer-service.js +41 -1
- package/dist/main.js +6 -0
- package/dist/managed-service-lifecycle.js +47 -2
- package/dist/managed-service-startup.js +2 -0
- package/dist/storage-runtime-controller.js +2 -0
- package/dist/storage-runtime-readiness.js +6 -0
- package/dist/storage-worker-executor.js +3 -0
- package/dist/storage-worker-operation.js +6 -0
- package/package.json +1 -1
package/dist/computer-cli.js
CHANGED
|
@@ -82,6 +82,14 @@ function translatedError(error, td) {
|
|
|
82
82
|
}
|
|
83
83
|
return td(error.message);
|
|
84
84
|
}
|
|
85
|
+
function storageServiceEnvironment(env) {
|
|
86
|
+
return {
|
|
87
|
+
...(env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: env.NOWWORK_HOME }),
|
|
88
|
+
...(env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: env.NOWWORK_ENV_TAG }),
|
|
89
|
+
...(env.NOWWORK_STORAGE_POLICY_PATH === undefined
|
|
90
|
+
? {} : { NOWWORK_STORAGE_POLICY_PATH: env.NOWWORK_STORAGE_POLICY_PATH }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
85
93
|
export async function runComputerCommand(argv, overrides = {}) {
|
|
86
94
|
const command = argv[0];
|
|
87
95
|
if (!command || !COMPUTER_COMMANDS.has(command))
|
|
@@ -202,6 +210,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
202
210
|
...deps,
|
|
203
211
|
profile: restartProfile.name,
|
|
204
212
|
profileHome: home,
|
|
213
|
+
environment: storageServiceEnvironment(deps.env),
|
|
205
214
|
})
|
|
206
215
|
: null;
|
|
207
216
|
if (spec && restartProfile) {
|
|
@@ -272,7 +281,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
272
281
|
throw new Error(td("Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry"));
|
|
273
282
|
}
|
|
274
283
|
const spec = buildServiceSpec({
|
|
275
|
-
...deps, profile: name, profileHome: home,
|
|
284
|
+
...deps, profile: name, profileHome: home, environment: storageServiceEnvironment(deps.env),
|
|
276
285
|
});
|
|
277
286
|
const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
|
|
278
287
|
const managedRecord = installation !== null && spec.platform !== "win32"
|
package/dist/computer-service.js
CHANGED
|
@@ -53,6 +53,7 @@ export function windowsCommandArg(value) {
|
|
|
53
53
|
}
|
|
54
54
|
export function buildServiceSpec(input) {
|
|
55
55
|
const { platform, profile, userHome } = input;
|
|
56
|
+
const environment = input.environment ?? process.env;
|
|
56
57
|
if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
|
|
57
58
|
throw new Error(`Computer services are not supported on platform '${platform}'`);
|
|
58
59
|
}
|
|
@@ -72,6 +73,16 @@ export function buildServiceSpec(input) {
|
|
|
72
73
|
throw new Error("Cannot install launchd service without a numeric uid");
|
|
73
74
|
const id = `com.nowcrew.daemon.${profile}${identitySuffix}`;
|
|
74
75
|
const args = daemonCommand.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
|
|
76
|
+
const environmentEntries = [
|
|
77
|
+
["NOWWORK_HOME", environment.NOWWORK_HOME],
|
|
78
|
+
["NOWWORK_ENV_TAG", environment.NOWWORK_ENV_TAG],
|
|
79
|
+
["NOWWORK_STORAGE_POLICY_PATH", environment.NOWWORK_STORAGE_POLICY_PATH],
|
|
80
|
+
].flatMap(([key, value]) => value === undefined
|
|
81
|
+
? []
|
|
82
|
+
: [` <key>${key}</key><string>${xml(assertSingleLine(value, key))}</string>`]);
|
|
83
|
+
const environmentBlock = environmentEntries.length === 0
|
|
84
|
+
? ""
|
|
85
|
+
: ` <key>EnvironmentVariables</key>\n <dict>\n${environmentEntries.join("\n")}\n </dict>\n`;
|
|
75
86
|
return {
|
|
76
87
|
platform,
|
|
77
88
|
profile,
|
|
@@ -79,7 +90,7 @@ export function buildServiceSpec(input) {
|
|
|
79
90
|
descriptorPath: resolve(userHome, "Library", "LaunchAgents", `${id}.plist`),
|
|
80
91
|
daemonCommand,
|
|
81
92
|
managerDomain: `gui/${input.uid}`,
|
|
82
|
-
descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
|
|
93
|
+
descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n${environmentBlock} <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
|
|
83
94
|
};
|
|
84
95
|
}
|
|
85
96
|
if (platform === "linux") {
|
|
@@ -266,6 +277,35 @@ export async function installService(spec, runner = systemCommandRunner) {
|
|
|
266
277
|
throw error;
|
|
267
278
|
}
|
|
268
279
|
}
|
|
280
|
+
export async function replaceInstalledServiceDescriptor(spec, previousDescriptor, runner = systemCommandRunner) {
|
|
281
|
+
if (spec.platform === "win32" || spec.descriptorPath === null || spec.descriptor === null) {
|
|
282
|
+
throw new Error("Installed service descriptor replacement requires launchd or systemd");
|
|
283
|
+
}
|
|
284
|
+
const current = await readServiceDescriptor(spec);
|
|
285
|
+
if (current !== previousDescriptor) {
|
|
286
|
+
throw new Error(`Installed descriptor for '${spec.id}' changed before replacement`);
|
|
287
|
+
}
|
|
288
|
+
const status = await serviceStatus(spec, runner);
|
|
289
|
+
if (!status.installed)
|
|
290
|
+
throw new Error(`Service '${spec.id}' is not installed`);
|
|
291
|
+
if (status.running || status.loaded)
|
|
292
|
+
await serviceAction(spec, "stop", runner);
|
|
293
|
+
try {
|
|
294
|
+
await writePrivateAtomic(spec.descriptorPath, spec.descriptor);
|
|
295
|
+
if (spec.platform === "linux") {
|
|
296
|
+
await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
|
|
297
|
+
await checked(runner, "systemctl", ["--user", "enable", spec.id], "systemd enable");
|
|
298
|
+
}
|
|
299
|
+
if (status.running)
|
|
300
|
+
await serviceAction(spec, "start", runner);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
await writePrivateAtomic(spec.descriptorPath, previousDescriptor).catch(() => undefined);
|
|
304
|
+
if (status.running)
|
|
305
|
+
await serviceAction(spec, "start", runner).catch(() => undefined);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
269
309
|
export async function ensureServiceEnabled(spec, runner = systemCommandRunner) {
|
|
270
310
|
if (spec.platform === "linux") {
|
|
271
311
|
if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
|
package/dist/main.js
CHANGED
|
@@ -85,6 +85,12 @@ async function main() {
|
|
|
85
85
|
agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
|
|
86
86
|
nodePath: process.execPath,
|
|
87
87
|
entryPath: builtDaemonEntry(),
|
|
88
|
+
environment: {
|
|
89
|
+
...(process.env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: process.env.NOWWORK_HOME }),
|
|
90
|
+
...(process.env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: process.env.NOWWORK_ENV_TAG }),
|
|
91
|
+
...(process.env.NOWWORK_STORAGE_POLICY_PATH === undefined
|
|
92
|
+
? {} : { NOWWORK_STORAGE_POLICY_PATH: process.env.NOWWORK_STORAGE_POLICY_PATH }),
|
|
93
|
+
},
|
|
88
94
|
});
|
|
89
95
|
}
|
|
90
96
|
applyProfileToEnv(profile, process.env);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { acquireDaemonInstallationLease } from "./daemon-installation-lease.js";
|
|
3
|
-
import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, uninstallService, } from "./computer-service.js";
|
|
3
|
+
import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, replaceInstalledServiceDescriptor, uninstallService, } from "./computer-service.js";
|
|
4
4
|
import { ManagedServiceConflictError, ManagedServiceRegistryError, assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, managedServiceConflict, normalizeManagedServiceRecord, serviceDescriptorSha256, withManagedServiceRegistryLock, } from "./managed-service-registry.js";
|
|
5
5
|
function requirePortableManagedSpec(spec) {
|
|
6
6
|
if ((spec.platform !== "darwin" && spec.platform !== "linux")
|
|
@@ -35,6 +35,17 @@ function sameManagedIdentity(left, right) {
|
|
|
35
35
|
const { generation: _generation, createdAt: _createdAt, ...expected } = right;
|
|
36
36
|
return managedServiceIdentityMatches(left, expected);
|
|
37
37
|
}
|
|
38
|
+
function sameManagedResource(left, right) {
|
|
39
|
+
const { generation: _leftGeneration, createdAt: _leftCreatedAt, descriptorSha256: _leftHash, ...leftResource } = left;
|
|
40
|
+
const { generation: _rightGeneration, createdAt: _rightCreatedAt, descriptorSha256: _rightHash, ...rightResource } = right;
|
|
41
|
+
return JSON.stringify(leftResource) === JSON.stringify(rightResource);
|
|
42
|
+
}
|
|
43
|
+
function descriptorDiffIsStorageEnvironmentOnly(previous, next) {
|
|
44
|
+
if (!next.includes("\n <key>EnvironmentVariables</key>\n"))
|
|
45
|
+
return false;
|
|
46
|
+
const strip = (descriptor) => descriptor.replace(/\n <key>EnvironmentVariables<\/key>\n <dict>\n(?: <key>(?:NOWWORK_HOME|NOWWORK_ENV_TAG|NOWWORK_STORAGE_POLICY_PATH)<\/key><string>[^<]*<\/string>\n)+ <\/dict>\n/u, "\n");
|
|
47
|
+
return strip(previous) === strip(next);
|
|
48
|
+
}
|
|
38
49
|
function assertRecordDescribesSpec(input) {
|
|
39
50
|
requirePortableManagedSpec(input.spec);
|
|
40
51
|
const { spec, record } = input;
|
|
@@ -51,8 +62,20 @@ function assertRecordDescribesSpec(input) {
|
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
export async function installManagedService(input) {
|
|
54
|
-
assertRecordDescribesSpec(input);
|
|
55
65
|
requirePortableManagedSpec(input.spec);
|
|
66
|
+
const requestedDescriptor = input.spec.descriptor;
|
|
67
|
+
const expectedDescriptorHash = serviceDescriptorSha256(input.spec.descriptor);
|
|
68
|
+
const expectedPath = normalizeManagedServiceRecord({
|
|
69
|
+
...input.record,
|
|
70
|
+
descriptorPath: input.spec.descriptorPath,
|
|
71
|
+
descriptorSha256: expectedDescriptorHash,
|
|
72
|
+
}).descriptorPath;
|
|
73
|
+
if (input.record.platform !== input.spec.platform
|
|
74
|
+
|| input.record.serviceId !== input.spec.id
|
|
75
|
+
|| input.record.profile !== input.spec.profile
|
|
76
|
+
|| input.record.descriptorPath !== expectedPath) {
|
|
77
|
+
throw new ManagedServiceRegistryError(`Managed service record does not describe '${input.spec.id}'`);
|
|
78
|
+
}
|
|
56
79
|
const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
|
|
57
80
|
await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services, replace }) => {
|
|
58
81
|
const exact = services.find((record) => sameManagedIdentity(record, input.record));
|
|
@@ -76,6 +99,28 @@ export async function installManagedService(input) {
|
|
|
76
99
|
}
|
|
77
100
|
return;
|
|
78
101
|
}
|
|
102
|
+
const registered = services.find((record) => record.serviceId === input.record.serviceId
|
|
103
|
+
&& record.profile === input.record.profile
|
|
104
|
+
&& record.daemonHome === input.record.daemonHome);
|
|
105
|
+
if (registered !== undefined && sameManagedResource(registered, input.record)) {
|
|
106
|
+
const current = await readServiceDescriptor(input.spec);
|
|
107
|
+
if (current === null || serviceDescriptorSha256(current) !== registered.descriptorSha256) {
|
|
108
|
+
throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
|
|
109
|
+
}
|
|
110
|
+
if (!descriptorDiffIsStorageEnvironmentOnly(current, requestedDescriptor)) {
|
|
111
|
+
throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' conflicts with the requested identity`);
|
|
112
|
+
}
|
|
113
|
+
await replaceInstalledServiceDescriptor(input.spec, current, input.runner);
|
|
114
|
+
try {
|
|
115
|
+
await replace(services.map((record) => record === registered ? input.record : record));
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const rollbackSpec = { ...input.spec, descriptor: current };
|
|
119
|
+
await replaceInstalledServiceDescriptor(rollbackSpec, requestedDescriptor, input.runner).catch(() => undefined);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
79
124
|
const conflict = managedServiceConflict(services, input.record);
|
|
80
125
|
if (conflict !== null) {
|
|
81
126
|
throw new ManagedServiceConflictError(conflict.resource, input.record, conflict.owner);
|
|
@@ -14,6 +14,7 @@ export async function assertManagedServiceStartup(input) {
|
|
|
14
14
|
nodePath: input.nodePath,
|
|
15
15
|
entryPath: input.entryPath,
|
|
16
16
|
profileHome: input.daemonHome,
|
|
17
|
+
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
17
18
|
});
|
|
18
19
|
const readRegistry = input.readManagedServices
|
|
19
20
|
?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
|
|
@@ -42,6 +43,7 @@ export async function assertManagedServiceStartup(input) {
|
|
|
42
43
|
nodePath: input.nodePath,
|
|
43
44
|
entryPath: input.entryPath,
|
|
44
45
|
profileHome: input.daemonHome,
|
|
46
|
+
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
45
47
|
legacyServiceId: true,
|
|
46
48
|
});
|
|
47
49
|
const legacyDescriptor = await readDescriptor(legacySpec);
|
|
@@ -67,6 +67,7 @@ function targetRecord(input, workspaceId, createdAt) {
|
|
|
67
67
|
nodePath: input.sourceRecord.nodePath,
|
|
68
68
|
entryPath: input.sourceRecord.entryPath,
|
|
69
69
|
profileHome: input.local.configRoot,
|
|
70
|
+
...(input.serviceEnvironment === undefined ? {} : { environment: input.serviceEnvironment }),
|
|
70
71
|
});
|
|
71
72
|
return buildManagedServiceRecord({
|
|
72
73
|
spec,
|
|
@@ -183,6 +184,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
|
|
|
183
184
|
platform: input.platform,
|
|
184
185
|
userHome: input.userHome,
|
|
185
186
|
...(input.uid === undefined ? {} : { uid: input.uid }),
|
|
187
|
+
...(input.serviceEnvironment === undefined ? {} : { serviceEnvironment: input.serviceEnvironment }),
|
|
186
188
|
sourceWasRunning: true,
|
|
187
189
|
sourceRecord: input.sourceRecord,
|
|
188
190
|
targetRecord: targetRecord(input, workspaceId, createdAt),
|
|
@@ -127,6 +127,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
127
127
|
nodePath: sourceRecord.nodePath,
|
|
128
128
|
entryPath: sourceRecord.entryPath,
|
|
129
129
|
profileHome: input.currentDaemonHome,
|
|
130
|
+
environment: input.environment,
|
|
130
131
|
});
|
|
131
132
|
const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
|
|
132
133
|
spec: sourceSpec,
|
|
@@ -168,6 +169,11 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
168
169
|
sourceRecord,
|
|
169
170
|
local,
|
|
170
171
|
policy,
|
|
172
|
+
serviceEnvironment: {
|
|
173
|
+
NOWWORK_HOME: nowworkHome,
|
|
174
|
+
NOWWORK_ENV_TAG: environment,
|
|
175
|
+
NOWWORK_STORAGE_POLICY_PATH: policyPath,
|
|
176
|
+
},
|
|
171
177
|
backupArtifacts: [
|
|
172
178
|
{ name: "profile.json", path: profilePath(input.profile, input.currentDaemonHome) },
|
|
173
179
|
{ name: "service-descriptor", path: sourceRecord.descriptorPath },
|
|
@@ -35,6 +35,9 @@ function serviceSpec(operation, record, profileHome) {
|
|
|
35
35
|
nodePath: record.nodePath,
|
|
36
36
|
entryPath: record.entryPath,
|
|
37
37
|
profileHome,
|
|
38
|
+
...(operation.service.serviceEnvironment === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: { environment: operation.service.serviceEnvironment }),
|
|
38
41
|
});
|
|
39
42
|
}
|
|
40
43
|
function assertRecordMatchesSpec(record, spec) {
|
|
@@ -90,6 +90,11 @@ const InstallationRecordSchema = z.object({
|
|
|
90
90
|
createdAt: z.string().datetime({ offset: true }),
|
|
91
91
|
updatedAt: z.string().datetime({ offset: true }),
|
|
92
92
|
}).strict();
|
|
93
|
+
const StorageServiceEnvironmentSchema = z.object({
|
|
94
|
+
NOWWORK_HOME: AbsolutePathSchema.optional(),
|
|
95
|
+
NOWWORK_ENV_TAG: z.string().regex(ENVIRONMENT).optional(),
|
|
96
|
+
NOWWORK_STORAGE_POLICY_PATH: AbsolutePathSchema.optional(),
|
|
97
|
+
}).strict();
|
|
93
98
|
const OperationSchema = z.object({
|
|
94
99
|
version: z.literal(1),
|
|
95
100
|
migrationId: z.string().uuid(),
|
|
@@ -119,6 +124,7 @@ const OperationSchema = z.object({
|
|
|
119
124
|
platform: z.enum(["darwin", "linux"]),
|
|
120
125
|
userHome: AbsolutePathSchema,
|
|
121
126
|
uid: z.number().int().nonnegative().optional(),
|
|
127
|
+
serviceEnvironment: StorageServiceEnvironmentSchema.optional(),
|
|
122
128
|
sourceWasRunning: z.boolean(),
|
|
123
129
|
sourceRecord: ManagedServiceRecordSchema,
|
|
124
130
|
targetRecord: ManagedServiceRecordSchema,
|