@nowcrew/daemon 0.5.12 → 0.5.14
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 +77 -0
- package/dist/config.js +38 -0
- package/dist/execution-event-limit.js +59 -0
- package/dist/execution-journal-lock.js +262 -0
- package/dist/execution-journal.js +678 -0
- package/dist/execution-protocol.js +310 -0
- package/dist/execution-runner.js +637 -0
- package/dist/execution-supervisor-child.js +185 -0
- package/dist/execution-supervisor.js +209 -0
- package/dist/local-executor.js +326 -0
- package/dist/machine-info.js +13 -4
- package/dist/origin-decision.js +42 -0
- package/dist/prompt.js +23 -1
- package/dist/runner.js +146 -340
- package/dist/runtimes/claude.js +9 -1
- package/dist/runtimes/codex.js +6 -1
- package/dist/runtimes/kimi.js +3 -0
- package/dist/scheduled-run-report.js +15 -7
- package/dist/serve.js +412 -57
- package/dist/token.js +5 -2
- package/dist/workspace.js +39 -11
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# NowWork Daemon
|
|
2
|
+
|
|
3
|
+
The daemon is NowWork's local execution adapter. It holds a machine credential, keeps one control-plane
|
|
4
|
+
WebSocket open, and launches coding runtimes on that machine. Product policy remains on the server.
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
Use the connection command generated by **Add Computer**:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx @nowcrew/daemon@latest --server-url https://nowwork.example --api-key sk_machine_...
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The process stays resident and reconnects with backoff. For local development, run
|
|
15
|
+
`pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
|
|
16
|
+
manual one-shot entry.
|
|
17
|
+
|
|
18
|
+
## Execution Boundary
|
|
19
|
+
|
|
20
|
+
The server selects the machine and sends a validated `execution:start` containing:
|
|
21
|
+
|
|
22
|
+
- fully rendered system and wake prompts;
|
|
23
|
+
- runtime/model/reasoning and timeout;
|
|
24
|
+
- requested permission and channel/thread identifiers;
|
|
25
|
+
- reporting flags for final text, activity, and console streams.
|
|
26
|
+
|
|
27
|
+
The daemon intersects requested permission with local policy, checks advertised resource limits, prepares
|
|
28
|
+
the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`, or
|
|
29
|
+
`kimi`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
|
|
30
|
+
behavior. Those are server responsibilities.
|
|
31
|
+
|
|
32
|
+
Protocol support and limits are advertised in `machine:hello`. Unknown required protocol semantics are
|
|
33
|
+
rejected before side effects. Protocol-0 `agent:start` remains only for the server-governed compatibility
|
|
34
|
+
window.
|
|
35
|
+
|
|
36
|
+
## Reliability
|
|
37
|
+
|
|
38
|
+
Protocol-v1 lifecycle is `accepted → started → completed`, followed by a server
|
|
39
|
+
`execution:completion-ack`. Reconnect uses `execution:sync`; cancellation uses `execution:cancel`.
|
|
40
|
+
Activity and console frames are best-effort, while lifecycle and terminal effects are durable on the
|
|
41
|
+
server.
|
|
42
|
+
|
|
43
|
+
Before accepting work, the daemon writes a local journal entry under:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
<CREW_AGENTS_ROOT>/.crew/executions/<executionId>.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`CREW_AGENTS_ROOT` defaults to `~/.crew/agents`. On restart, accepted entries become interrupted failures;
|
|
50
|
+
running supervisors are identity-checked and terminated before interruption is reported. A lock prevents
|
|
51
|
+
two daemon processes from sharing one journal.
|
|
52
|
+
|
|
53
|
+
## Local Policy
|
|
54
|
+
|
|
55
|
+
Useful environment controls:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
CREW_RUNTIME_SAFE=1
|
|
59
|
+
CREW_EXECUTION_MAX_PROMPT_BYTES=256000
|
|
60
|
+
CREW_EXECUTION_MAX_TIMEOUT_MS=3600000
|
|
61
|
+
CREW_EXECUTION_MAX_EVENT_BYTES=64000
|
|
62
|
+
CREW_MAX_PARALLEL=4
|
|
63
|
+
CREW_EXECUTION_MAX_QUEUED_PER_AGENT=32
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Local policy can reduce server-requested access and limits; it cannot grant more access than requested.
|
|
67
|
+
Provider credentials and configured environment are prepared locally and never carried in execution
|
|
68
|
+
control frames.
|
|
69
|
+
|
|
70
|
+
## Code Map
|
|
71
|
+
|
|
72
|
+
- `src/serve.ts`: connection, negotiation, routing, sync, and legacy boundary.
|
|
73
|
+
- `src/execution-runner.ts`: spec admission and lifecycle reporting.
|
|
74
|
+
- `src/local-executor.ts` and `src/execution-supervisor.ts`: runtime process boundary.
|
|
75
|
+
- `src/execution-journal.ts`: crash-safe local execution facts.
|
|
76
|
+
- `src/runner.ts`: protocol-0 compatibility runner.
|
|
77
|
+
- `src/runtimes/`: built-in runtime adapters.
|
package/dist/config.js
CHANGED
|
@@ -4,8 +4,38 @@ import { dirname, resolve } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
7
|
+
export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
|
|
8
|
+
maxPromptBytes: 256_000,
|
|
9
|
+
maxTimeoutMs: 3_600_000,
|
|
10
|
+
maxEventBytes: 64_000,
|
|
11
|
+
maxParallelPerAgent: 4,
|
|
12
|
+
maxQueuedPerAgent: 32,
|
|
13
|
+
});
|
|
14
|
+
// Fits the largest mandatory v1 lifecycle envelope (UUID + timestamps + outcome facts) with margin.
|
|
15
|
+
export const MIN_EXECUTION_EVENT_BYTES = 512;
|
|
16
|
+
export const MAX_EXECUTION_TIMEOUT_MS = 2_147_483_647;
|
|
7
17
|
export class ConfigError extends Error {
|
|
8
18
|
}
|
|
19
|
+
function positiveIntegerEnv(env, field, fallback) {
|
|
20
|
+
const raw = env[field];
|
|
21
|
+
const value = raw === undefined ? fallback : Number(raw);
|
|
22
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
23
|
+
throw new ConfigError(`${field} must be a finite positive integer`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function integerEnvAtLeast(env, field, fallback, minimum) {
|
|
28
|
+
const value = positiveIntegerEnv(env, field, fallback);
|
|
29
|
+
if (value < minimum)
|
|
30
|
+
throw new ConfigError(`${field} must be at least ${minimum}`);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function integerEnvAtMost(env, field, fallback, maximum) {
|
|
34
|
+
const value = positiveIntegerEnv(env, field, fallback);
|
|
35
|
+
if (value > maximum)
|
|
36
|
+
throw new ConfigError(`${field} must be at most ${maximum}`);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
9
39
|
function defaultCliPath() {
|
|
10
40
|
// 已发布场景(npx):crew CLI 是 daemon 的依赖,从 node_modules 解析 @nowcrew/cli。
|
|
11
41
|
try {
|
|
@@ -24,6 +54,13 @@ export function loadConfig(env = process.env) {
|
|
|
24
54
|
const lang = detectDaemonLang(env);
|
|
25
55
|
throw new ConfigError(translateDaemon(lang, "Missing CREW_MACHINE_TOKEN (sk_machine_*, printed by seed)"));
|
|
26
56
|
}
|
|
57
|
+
const executionLimits = Object.freeze({
|
|
58
|
+
maxPromptBytes: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_PROMPT_BYTES", DEFAULT_EXECUTION_LIMITS.maxPromptBytes),
|
|
59
|
+
maxTimeoutMs: integerEnvAtMost(env, "CREW_EXECUTION_MAX_TIMEOUT_MS", DEFAULT_EXECUTION_LIMITS.maxTimeoutMs, MAX_EXECUTION_TIMEOUT_MS),
|
|
60
|
+
maxEventBytes: integerEnvAtLeast(env, "CREW_EXECUTION_MAX_EVENT_BYTES", DEFAULT_EXECUTION_LIMITS.maxEventBytes, MIN_EXECUTION_EVENT_BYTES),
|
|
61
|
+
maxParallelPerAgent: positiveIntegerEnv(env, "CREW_MAX_PARALLEL", DEFAULT_EXECUTION_LIMITS.maxParallelPerAgent),
|
|
62
|
+
maxQueuedPerAgent: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_QUEUED_PER_AGENT", DEFAULT_EXECUTION_LIMITS.maxQueuedPerAgent),
|
|
63
|
+
});
|
|
27
64
|
return {
|
|
28
65
|
serverUrl,
|
|
29
66
|
machineToken,
|
|
@@ -38,5 +75,6 @@ export function loadConfig(env = process.env) {
|
|
|
38
75
|
sessionSoftTokens: env.CREW_SESSION_SOFT_TOKENS != null ? Number(env.CREW_SESSION_SOFT_TOKENS) : 90_000,
|
|
39
76
|
sessionMaxTurns: env.CREW_SESSION_MAX_TURNS != null ? Number(env.CREW_SESSION_MAX_TURNS) : 30,
|
|
40
77
|
productName: env.CREW_PRODUCT_NAME ?? "nowwork",
|
|
78
|
+
executionLimits,
|
|
41
79
|
};
|
|
42
80
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DaemonToServerExecutionFrameSchema, } from "./execution-protocol.js";
|
|
2
|
+
export function executionFrameBytes(frame) {
|
|
3
|
+
return Buffer.byteLength(JSON.stringify(frame), "utf8");
|
|
4
|
+
}
|
|
5
|
+
function withBoundedString(frame, field, maxBytes, optional, minimumCharacters = 0) {
|
|
6
|
+
const raw = frame[field];
|
|
7
|
+
if (typeof raw !== "string" || executionFrameBytes(frame) <= maxBytes)
|
|
8
|
+
return frame;
|
|
9
|
+
const characters = Array.from(raw);
|
|
10
|
+
let low = 0;
|
|
11
|
+
let high = characters.length;
|
|
12
|
+
let best = null;
|
|
13
|
+
while (low <= high) {
|
|
14
|
+
const middle = Math.floor((low + high) / 2);
|
|
15
|
+
const candidate = { ...frame, [field]: characters.slice(0, middle).join("") };
|
|
16
|
+
if (middle >= minimumCharacters && executionFrameBytes(candidate) <= maxBytes) {
|
|
17
|
+
best = candidate;
|
|
18
|
+
low = middle + 1;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
high = middle - 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (best !== null && (!optional || best[field] !== ""))
|
|
25
|
+
return best;
|
|
26
|
+
if (optional) {
|
|
27
|
+
const { [field]: _omitted, ...withoutField } = frame;
|
|
28
|
+
return withoutField;
|
|
29
|
+
}
|
|
30
|
+
return best ?? frame;
|
|
31
|
+
}
|
|
32
|
+
export function boundExecutionFrame(input, maxBytes) {
|
|
33
|
+
if (executionFrameBytes(input) <= maxBytes)
|
|
34
|
+
return input;
|
|
35
|
+
let frame = { ...input };
|
|
36
|
+
if (input.type === "execution:activity") {
|
|
37
|
+
frame = withBoundedString(frame, "detail", maxBytes, false);
|
|
38
|
+
}
|
|
39
|
+
else if (input.type === "execution:console") {
|
|
40
|
+
frame = withBoundedString(frame, "text", maxBytes, false);
|
|
41
|
+
}
|
|
42
|
+
else if (input.type === "execution:rejected") {
|
|
43
|
+
frame = withBoundedString(frame, "message", maxBytes, true);
|
|
44
|
+
}
|
|
45
|
+
else if (input.type === "execution:completed") {
|
|
46
|
+
for (const field of ["finalText", "errorMessage", "model", "terminationSignal"]) {
|
|
47
|
+
frame = withBoundedString(frame, field, maxBytes, true);
|
|
48
|
+
}
|
|
49
|
+
if (executionFrameBytes(frame) > maxBytes && "usage" in frame) {
|
|
50
|
+
const { usage: _usage, ...withoutUsage } = frame;
|
|
51
|
+
frame = withoutUsage;
|
|
52
|
+
}
|
|
53
|
+
frame = withBoundedString(frame, "errorCode", maxBytes, false, 1);
|
|
54
|
+
}
|
|
55
|
+
if (executionFrameBytes(frame) > maxBytes) {
|
|
56
|
+
throw new RangeError(`Mandatory ${input.type} envelope exceeds maxEventBytes=${maxBytes}`);
|
|
57
|
+
}
|
|
58
|
+
return DaemonToServerExecutionFrameSchema.parse(frame);
|
|
59
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { mkdir, open, readFile, readdir, rename, rm, rmdir, stat, unlink, } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const OwnerSchema = z.object({
|
|
6
|
+
pid: z.number().int().positive(),
|
|
7
|
+
processIdentity: z.string().min(1),
|
|
8
|
+
token: z.string().uuid(),
|
|
9
|
+
}).strict();
|
|
10
|
+
export class JournalLockedError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "JournalLockedError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class JournalLockCorruptionError extends Error {
|
|
17
|
+
constructor(path, cause) {
|
|
18
|
+
super(`Invalid execution journal lock: ${path}`, { cause });
|
|
19
|
+
this.name = "JournalLockCorruptionError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const defaultJournalLockFileSystem = {
|
|
23
|
+
mkdir,
|
|
24
|
+
readFile: (path) => readFile(path, "utf8"),
|
|
25
|
+
readdir,
|
|
26
|
+
rename,
|
|
27
|
+
rm,
|
|
28
|
+
rmdir,
|
|
29
|
+
stat,
|
|
30
|
+
unlink,
|
|
31
|
+
writeDurableFile: async (path, contents) => {
|
|
32
|
+
const handle = await open(path, "wx", 0o600);
|
|
33
|
+
try {
|
|
34
|
+
await handle.writeFile(contents, "utf8");
|
|
35
|
+
await handle.sync();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await handle.close();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const defaultRegistry = { leases: new Map() };
|
|
43
|
+
export function createJournalLeaseRegistry() {
|
|
44
|
+
return { leases: new Map() };
|
|
45
|
+
}
|
|
46
|
+
const codeOf = (error) => error instanceof Error && "code" in error ? error.code : undefined;
|
|
47
|
+
function processLeaseMap(registry) {
|
|
48
|
+
return registry.leases;
|
|
49
|
+
}
|
|
50
|
+
export function createJournalLease(options) {
|
|
51
|
+
const fileSystem = options.fileSystem ?? defaultJournalLockFileSystem;
|
|
52
|
+
const registry = processLeaseMap(options.registry ?? defaultRegistry);
|
|
53
|
+
const now = options.now ?? (() => new Date());
|
|
54
|
+
const orphanGraceMs = options.orphanGraceMs ?? 30_000;
|
|
55
|
+
const lockDirectory = join(options.directory, ".journal.lock");
|
|
56
|
+
let attached = false;
|
|
57
|
+
let status = "open";
|
|
58
|
+
const ownerFileName = (token) => `owner.${token}.json`;
|
|
59
|
+
const parseOwner = async (fileName) => {
|
|
60
|
+
const path = join(lockDirectory, fileName);
|
|
61
|
+
try {
|
|
62
|
+
const owner = OwnerSchema.parse(JSON.parse(await fileSystem.readFile(path)));
|
|
63
|
+
if (fileName !== ownerFileName(owner.token))
|
|
64
|
+
throw new Error("owner token does not match filename");
|
|
65
|
+
return owner;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new JournalLockCorruptionError(path, error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const readOwner = async () => {
|
|
72
|
+
let names;
|
|
73
|
+
try {
|
|
74
|
+
names = (await fileSystem.readdir(lockDirectory)).map(String).sort();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (codeOf(error) === "ENOENT")
|
|
78
|
+
return null;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (names.length === 0) {
|
|
82
|
+
const lockStat = await fileSystem.stat(lockDirectory);
|
|
83
|
+
if (now().valueOf() - lockStat.mtimeMs < orphanGraceMs) {
|
|
84
|
+
throw new JournalLockedError("Execution journal lock owner installation is in progress");
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await fileSystem.rmdir(lockDirectory);
|
|
88
|
+
await options.syncDirectory(options.directory);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (names.length !== 1 || !/^owner\.[0-9a-f-]+\.json$/i.test(names[0])) {
|
|
97
|
+
throw new JournalLockCorruptionError(lockDirectory, new Error("lock directory must contain one owner"));
|
|
98
|
+
}
|
|
99
|
+
return { owner: await parseOwner(names[0]), fileName: names[0] };
|
|
100
|
+
};
|
|
101
|
+
const validateInstalledOwner = async (lease) => {
|
|
102
|
+
const observed = await readOwner();
|
|
103
|
+
if (observed === null
|
|
104
|
+
|| observed.fileName !== lease.ownerFile
|
|
105
|
+
|| JSON.stringify(observed.owner) !== JSON.stringify(lease.owner)) {
|
|
106
|
+
throw new JournalLockCorruptionError(lockDirectory, new Error("installed owner changed"));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const finishInstall = async (lease) => {
|
|
110
|
+
if (!lease.ownerInstalled) {
|
|
111
|
+
try {
|
|
112
|
+
await fileSystem.rename(lease.ownerTemp, join(lockDirectory, lease.ownerFile));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (codeOf(error) !== "ENOENT")
|
|
116
|
+
throw error;
|
|
117
|
+
await validateInstalledOwner(lease);
|
|
118
|
+
}
|
|
119
|
+
lease.ownerInstalled = true;
|
|
120
|
+
}
|
|
121
|
+
await validateInstalledOwner(lease);
|
|
122
|
+
if (!lease.lockDirectorySynced) {
|
|
123
|
+
await options.syncDirectory(lockDirectory);
|
|
124
|
+
lease.lockDirectorySynced = true;
|
|
125
|
+
}
|
|
126
|
+
if (!lease.executionsDirectorySynced) {
|
|
127
|
+
await options.syncDirectory(options.directory);
|
|
128
|
+
lease.executionsDirectorySynced = true;
|
|
129
|
+
}
|
|
130
|
+
await fileSystem.rm(lease.ownerTemp, { force: true }).catch(() => undefined);
|
|
131
|
+
};
|
|
132
|
+
const install = async () => {
|
|
133
|
+
const token = randomUUID();
|
|
134
|
+
const owner = OwnerSchema.parse({
|
|
135
|
+
pid: options.currentPid,
|
|
136
|
+
processIdentity: await options.captureCurrentIdentity(),
|
|
137
|
+
token,
|
|
138
|
+
});
|
|
139
|
+
const ownerTemp = join(options.directory, `.journal-owner.${token}.tmp`);
|
|
140
|
+
await fileSystem.writeDurableFile(ownerTemp, `${JSON.stringify(owner)}\n`);
|
|
141
|
+
try {
|
|
142
|
+
for (;;) {
|
|
143
|
+
try {
|
|
144
|
+
await fileSystem.mkdir(lockDirectory);
|
|
145
|
+
const lease = {
|
|
146
|
+
owner,
|
|
147
|
+
ownerFile: ownerFileName(token),
|
|
148
|
+
ownerTemp,
|
|
149
|
+
refs: 1,
|
|
150
|
+
ownerInstalled: false,
|
|
151
|
+
lockDirectorySynced: false,
|
|
152
|
+
executionsDirectorySynced: false,
|
|
153
|
+
releasing: false,
|
|
154
|
+
ownerRemoved: false,
|
|
155
|
+
lockDirectoryRemoved: false,
|
|
156
|
+
releaseSynced: false,
|
|
157
|
+
};
|
|
158
|
+
registry.set(options.directory, lease);
|
|
159
|
+
attached = true;
|
|
160
|
+
await finishInstall(lease);
|
|
161
|
+
return lease;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (codeOf(error) !== "EEXIST")
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
const observed = await readOwner();
|
|
168
|
+
if (observed === null)
|
|
169
|
+
continue;
|
|
170
|
+
const identity = await options.inspectIdentity(observed.owner.pid);
|
|
171
|
+
if (identity === observed.owner.processIdentity) {
|
|
172
|
+
throw new JournalLockedError(`Execution journal is locked by process ${observed.owner.pid}`);
|
|
173
|
+
}
|
|
174
|
+
await options.hooks?.beforeRemoveObservedOwner?.(observed.owner);
|
|
175
|
+
try {
|
|
176
|
+
await fileSystem.unlink(join(lockDirectory, observed.fileName));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (codeOf(error) !== "ENOENT")
|
|
180
|
+
throw error;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await fileSystem.rmdir(lockDirectory);
|
|
185
|
+
await options.syncDirectory(options.directory);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (codeOf(error) !== "ENOENT" && codeOf(error) !== "ENOTEMPTY")
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
if (!attached)
|
|
195
|
+
await fileSystem.rm(ownerTemp, { force: true }).catch(() => undefined);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const acquire = async () => {
|
|
199
|
+
if (status !== "open")
|
|
200
|
+
throw new JournalLockedError(`Journal lease is ${status}`);
|
|
201
|
+
if (attached) {
|
|
202
|
+
const lease = registry.get(options.directory);
|
|
203
|
+
if (lease === undefined)
|
|
204
|
+
throw new JournalLockedError("In-process journal lease is missing");
|
|
205
|
+
await finishInstall(lease);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const existing = registry.get(options.directory);
|
|
209
|
+
if (existing !== undefined) {
|
|
210
|
+
if (existing.releasing)
|
|
211
|
+
throw new JournalLockedError("Journal lease release is pending");
|
|
212
|
+
await finishInstall(existing);
|
|
213
|
+
existing.refs += 1;
|
|
214
|
+
attached = true;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await install();
|
|
218
|
+
};
|
|
219
|
+
const close = async () => {
|
|
220
|
+
if (status === "closed")
|
|
221
|
+
return;
|
|
222
|
+
if (!attached) {
|
|
223
|
+
status = "closed";
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const lease = registry.get(options.directory);
|
|
227
|
+
if (lease === undefined)
|
|
228
|
+
throw new JournalLockedError("In-process journal lease is missing");
|
|
229
|
+
if (status === "open" && lease.refs > 1) {
|
|
230
|
+
lease.refs -= 1;
|
|
231
|
+
attached = false;
|
|
232
|
+
status = "closed";
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
status = "closing";
|
|
236
|
+
lease.releasing = true;
|
|
237
|
+
if (!lease.ownerRemoved) {
|
|
238
|
+
await fileSystem.unlink(join(lockDirectory, lease.ownerFile));
|
|
239
|
+
lease.ownerRemoved = true;
|
|
240
|
+
}
|
|
241
|
+
if (!lease.lockDirectoryRemoved) {
|
|
242
|
+
await fileSystem.rmdir(lockDirectory);
|
|
243
|
+
lease.lockDirectoryRemoved = true;
|
|
244
|
+
}
|
|
245
|
+
if (!lease.releaseSynced) {
|
|
246
|
+
await options.syncDirectory(options.directory);
|
|
247
|
+
lease.releaseSynced = true;
|
|
248
|
+
}
|
|
249
|
+
lease.refs -= 1;
|
|
250
|
+
registry.delete(options.directory);
|
|
251
|
+
attached = false;
|
|
252
|
+
status = "closed";
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
acquire,
|
|
256
|
+
assertUsable: () => {
|
|
257
|
+
if (status !== "open")
|
|
258
|
+
throw new JournalLockedError(`Execution journal lease is ${status}`);
|
|
259
|
+
},
|
|
260
|
+
close,
|
|
261
|
+
};
|
|
262
|
+
}
|