@nowcrew/daemon 0.6.57 → 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 +3 -0
- package/dist/storage-runtime-readiness.js +10 -1
- package/dist/storage-worker-executor.js +3 -0
- package/dist/storage-worker-handoff.js +39 -4
- 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),
|
|
@@ -236,6 +238,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
|
|
|
236
238
|
migrationId: operation.migrationId,
|
|
237
239
|
platform: input.platform,
|
|
238
240
|
userHome: input.userHome,
|
|
241
|
+
...(input.uid === undefined ? {} : { uid: input.uid }),
|
|
239
242
|
});
|
|
240
243
|
workerLaunched = true;
|
|
241
244
|
const outcome = await dependencies.monitorWorker({
|
|
@@ -58,9 +58,12 @@ function findSourceRecord(records, input) {
|
|
|
58
58
|
return matches.length === 1 ? matches[0] : null;
|
|
59
59
|
}
|
|
60
60
|
export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
61
|
-
if (rawInput.platform !== "linux") {
|
|
61
|
+
if (rawInput.platform !== "darwin" && rawInput.platform !== "linux") {
|
|
62
62
|
return notReady("platform_not_supported");
|
|
63
63
|
}
|
|
64
|
+
if (rawInput.platform === "darwin" && rawInput.uid === undefined) {
|
|
65
|
+
return notReady("platform_identity_missing");
|
|
66
|
+
}
|
|
64
67
|
const configuredNowworkHome = rawInput.environment.NOWWORK_HOME;
|
|
65
68
|
if (configuredNowworkHome === undefined)
|
|
66
69
|
return notReady("nowwork_home_missing");
|
|
@@ -124,6 +127,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
124
127
|
nodePath: sourceRecord.nodePath,
|
|
125
128
|
entryPath: sourceRecord.entryPath,
|
|
126
129
|
profileHome: input.currentDaemonHome,
|
|
130
|
+
environment: input.environment,
|
|
127
131
|
});
|
|
128
132
|
const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
|
|
129
133
|
spec: sourceSpec,
|
|
@@ -165,6 +169,11 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
165
169
|
sourceRecord,
|
|
166
170
|
local,
|
|
167
171
|
policy,
|
|
172
|
+
serviceEnvironment: {
|
|
173
|
+
NOWWORK_HOME: nowworkHome,
|
|
174
|
+
NOWWORK_ENV_TAG: environment,
|
|
175
|
+
NOWWORK_STORAGE_POLICY_PATH: policyPath,
|
|
176
|
+
},
|
|
168
177
|
backupArtifacts: [
|
|
169
178
|
{ name: "profile.json", path: profilePath(input.profile, input.currentDaemonHome) },
|
|
170
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) {
|
|
@@ -62,6 +62,16 @@ function workerUnitName(migrationId) {
|
|
|
62
62
|
function workerUnitPath(userHome, migrationId) {
|
|
63
63
|
return resolve(userHome, ".config", "systemd", "user", workerUnitName(migrationId));
|
|
64
64
|
}
|
|
65
|
+
function workerLaunchdLabel(migrationId) {
|
|
66
|
+
return `com.nowcrew.storage-migration.${migrationId}`;
|
|
67
|
+
}
|
|
68
|
+
function workerLaunchdPath(userHome, migrationId) {
|
|
69
|
+
return resolve(userHome, "Library", "LaunchAgents", `${workerLaunchdLabel(migrationId)}.plist`);
|
|
70
|
+
}
|
|
71
|
+
function xml(value) {
|
|
72
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
73
|
+
.replaceAll('"', """).replaceAll("'", "'");
|
|
74
|
+
}
|
|
65
75
|
function systemdArg(value) {
|
|
66
76
|
if (/[\0\r\n]/u.test(value))
|
|
67
77
|
throw new Error("storage worker service argument must be a single line");
|
|
@@ -77,9 +87,26 @@ export async function launchStorageMigrationWorker(input, runSupervisor = system
|
|
|
77
87
|
AbsolutePathSchema.parse(input.handoffPath);
|
|
78
88
|
AbsolutePathSchema.parse(input.userHome);
|
|
79
89
|
const migrationId = z.string().uuid().parse(input.migrationId).toLowerCase();
|
|
80
|
-
if (input.platform !== "linux")
|
|
81
|
-
throw new Error("Persistent storage migration workers require Linux systemd");
|
|
82
90
|
const worker = [process.execPath, input.entryPath, "storage-migration-worker", "--job", input.handoffPath];
|
|
91
|
+
if (input.platform === "darwin") {
|
|
92
|
+
if (input.uid === undefined || !Number.isSafeInteger(input.uid) || input.uid < 0) {
|
|
93
|
+
throw new Error("Persistent storage migration launchd worker requires a numeric uid");
|
|
94
|
+
}
|
|
95
|
+
const label = workerLaunchdLabel(migrationId);
|
|
96
|
+
const path = workerLaunchdPath(input.userHome, migrationId);
|
|
97
|
+
const args = worker.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
|
|
98
|
+
const 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(label)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>RunAtLoad</key><true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key><false/>\n </dict>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`;
|
|
99
|
+
await atomicPrivateWrite(path, descriptor);
|
|
100
|
+
try {
|
|
101
|
+
await checkedSupervisor(runSupervisor, "launchctl", ["bootstrap", `gui/${input.uid}`, path]);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
await runSupervisor("launchctl", ["bootout", `gui/${input.uid}/${label}`]).catch(() => undefined);
|
|
105
|
+
await rm(path, { force: true });
|
|
106
|
+
throw new Error("Storage migration persistent service launch failed", { cause: error });
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
83
110
|
const unit = workerUnitName(migrationId);
|
|
84
111
|
const path = workerUnitPath(input.userHome, migrationId);
|
|
85
112
|
const descriptor = `[Unit]\nDescription=NowWork storage migration ${migrationId}\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=exec\nExecStart=${worker.map(systemdArg).join(" ")}\nRestart=on-abnormal\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`;
|
|
@@ -97,9 +124,16 @@ export async function launchStorageMigrationWorker(input, runSupervisor = system
|
|
|
97
124
|
}
|
|
98
125
|
export async function completeStorageMigrationWorkerService(input, runSupervisor = systemSupervisorRun) {
|
|
99
126
|
const migrationId = z.string().uuid().parse(input.migrationId).toLowerCase();
|
|
100
|
-
if (input.platform !== "linux")
|
|
101
|
-
return;
|
|
102
127
|
AbsolutePathSchema.parse(input.userHome);
|
|
128
|
+
if (input.platform === "darwin") {
|
|
129
|
+
if (input.uid === undefined || !Number.isSafeInteger(input.uid) || input.uid < 0) {
|
|
130
|
+
throw new Error("Persistent storage migration launchd worker requires a numeric uid");
|
|
131
|
+
}
|
|
132
|
+
const label = workerLaunchdLabel(migrationId);
|
|
133
|
+
await rm(workerLaunchdPath(input.userHome, migrationId), { force: true });
|
|
134
|
+
await runSupervisor("launchctl", ["bootout", `gui/${input.uid}/${label}`]);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
103
137
|
const unit = workerUnitName(migrationId);
|
|
104
138
|
await checkedSupervisor(runSupervisor, "systemctl", ["--user", "disable", unit]);
|
|
105
139
|
await rm(workerUnitPath(input.userHome, migrationId), { force: true });
|
|
@@ -112,5 +146,6 @@ export async function completeStorageMigrationWorkerServiceForHandoff(path) {
|
|
|
112
146
|
migrationId: operation.migrationId,
|
|
113
147
|
platform: operation.service.platform,
|
|
114
148
|
userHome: operation.service.userHome,
|
|
149
|
+
...(operation.service.uid === undefined ? {} : { uid: operation.service.uid }),
|
|
115
150
|
});
|
|
116
151
|
}
|
|
@@ -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,
|