@solaqua/gji 0.12.0 → 0.12.2
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/bootstrap-output.js +0 -1
- package/dist/dependency-bootstrap.d.ts +10 -17
- package/dist/dependency-bootstrap.js +343 -108
- package/dist/dir-clone.d.ts +1 -2
- package/dist/dir-clone.js +303 -167
- package/dist/gji-bundle.mjs +985 -799
- package/dist/hub.js +13 -1
- package/dist/safe-destination.js +10 -0
- package/dist/sync-directories.d.ts +0 -4
- package/dist/sync-directories.js +6 -26
- package/dist/worktree-bootstrap.js +0 -2
- package/dist/worktree-picker.js +13 -0
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +1 -1
- package/man/man1/gji-undo.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +1 -1
- package/package.json +1 -1
- package/dist/clone-failure-store.d.ts +0 -18
- package/dist/clone-failure-store.js +0 -230
package/dist/dir-clone.d.ts
CHANGED
|
@@ -8,12 +8,11 @@ export interface CloneRequestOptions {
|
|
|
8
8
|
}
|
|
9
9
|
export interface CloneDirOptions extends CloneRequestOptions {
|
|
10
10
|
platform?: NodeJS.Platform;
|
|
11
|
-
|
|
11
|
+
runLinuxCommand?: (command: string, args: string[]) => Promise<void>;
|
|
12
12
|
copyDirectory?: (source: string, destination: string) => Promise<void>;
|
|
13
13
|
copyFile?: (source: string, destination: string) => Promise<void>;
|
|
14
14
|
}
|
|
15
15
|
export type CloneDirectory = (source: string, destination: string, options?: CloneRequestOptions) => Promise<CloneDirResult>;
|
|
16
|
-
export declare function waitForCloneLock(destination: string, timeoutMs?: number): Promise<boolean>;
|
|
17
16
|
export declare function cloneDir(source: string, destination: string, options?: CloneDirOptions): Promise<CloneDirResult>;
|
|
18
17
|
export declare class CloneDestinationExistsError extends Error {
|
|
19
18
|
readonly code = "GJI_CLONE_DESTINATION_EXISTS";
|
package/dist/dir-clone.js
CHANGED
|
@@ -2,54 +2,18 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { chmod, cp, lstat, mkdir, mkdtemp, opendir, readdir, readFile, readlink, realpath, rename, rm, rmdir, symlink, unlink, utimes, writeFile, } from "node:fs/promises";
|
|
5
|
-
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { isAlreadyExistsError, isNotFoundError } from "./fs-utils.js";
|
|
8
8
|
import { inspectDestination, openDestinationDirectory, } from "./safe-destination.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
|
-
const CLONE_LOCK_TTL_MS =
|
|
10
|
+
const CLONE_LOCK_TTL_MS = 5 * 60 * 1000;
|
|
11
|
+
const CLONE_GUARD_TTL_MS = 30 * 1000;
|
|
11
12
|
const CLONE_LOCK_SUFFIX = ".gji-clone-lock";
|
|
12
13
|
const SIZE_ESTIMATE_MAX_ENTRIES = 1_000_000;
|
|
13
14
|
const SIZE_ESTIMATE_MAX_MS = 5_000;
|
|
14
|
-
export async function waitForCloneLock(destination, timeoutMs = 5_000) {
|
|
15
|
-
const lockPath = `${destination}${CLONE_LOCK_SUFFIX}`;
|
|
16
|
-
const deadline = Date.now() + timeoutMs;
|
|
17
|
-
while (await cloneLockExists(lockPath)) {
|
|
18
|
-
if (await cloneLockIsStale(lockPath))
|
|
19
|
-
return true;
|
|
20
|
-
if (Date.now() >= deadline)
|
|
21
|
-
return false;
|
|
22
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
23
|
-
}
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
async function cloneLockIsStale(lockPath) {
|
|
27
|
-
try {
|
|
28
|
-
const freshnessPath = await lockFreshnessPath(lockPath);
|
|
29
|
-
const stats = await lstat(freshnessPath);
|
|
30
|
-
return Date.now() - stats.mtimeMs >= CLONE_LOCK_TTL_MS;
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (isNotFoundError(error))
|
|
34
|
-
return false;
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function cloneLockExists(lockPath) {
|
|
39
|
-
try {
|
|
40
|
-
return await destinationExists(lockPath);
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if ("code" in error &&
|
|
44
|
-
error.code === "ENOTDIR") {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
15
|
export async function cloneDir(source, destination, options = {}) {
|
|
51
16
|
const platform = options.platform ?? process.platform;
|
|
52
|
-
const strategy = cloneStrategy(platform);
|
|
53
17
|
if (!isClonePlatformSupported(platform)) {
|
|
54
18
|
throw new CloneUnsupportedError(`platform ${platform} has no CoW strategy`);
|
|
55
19
|
}
|
|
@@ -58,8 +22,12 @@ export async function cloneDir(source, destination, options = {}) {
|
|
|
58
22
|
if (!sourceStats.isDirectory()) {
|
|
59
23
|
throw new Error("source is not a directory");
|
|
60
24
|
}
|
|
61
|
-
|
|
62
|
-
|
|
25
|
+
const destinationPath = await resolveProspectivePath(destination);
|
|
26
|
+
const destinationDistance = relative(sourcePath, destinationPath);
|
|
27
|
+
if (destinationDistance === "" ||
|
|
28
|
+
(destinationDistance !== ".." &&
|
|
29
|
+
!destinationDistance.startsWith(`..${sep}`))) {
|
|
30
|
+
throw new Error("clone destination must not be inside its source");
|
|
63
31
|
}
|
|
64
32
|
const startedAt = Date.now();
|
|
65
33
|
const parent = dirname(destination);
|
|
@@ -77,78 +45,87 @@ export async function cloneDir(source, destination, options = {}) {
|
|
|
77
45
|
}
|
|
78
46
|
const operationParent = safeParent?.path ?? parent;
|
|
79
47
|
const operationDestination = join(operationParent, basename(destination));
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
const lockPath = `${destination}${CLONE_LOCK_SUFFIX}`;
|
|
84
|
-
const lockToken = await acquireCloneLock(lockPath, destination);
|
|
85
|
-
const stopLockHeartbeat = startLockHeartbeat(lockPath, lockToken);
|
|
48
|
+
const cloneLock = await acquireCloneLock(`${operationDestination}${CLONE_LOCK_SUFFIX}`, destination);
|
|
49
|
+
const stopLockHeartbeat = startLockHeartbeat(cloneLock);
|
|
86
50
|
let temporaryRoot;
|
|
87
|
-
let
|
|
88
|
-
const
|
|
51
|
+
let reservation;
|
|
52
|
+
const publishedEntries = [];
|
|
89
53
|
try {
|
|
90
|
-
|
|
54
|
+
if (await destinationExists(operationDestination)) {
|
|
55
|
+
throw new CloneDestinationExistsError(destination);
|
|
56
|
+
}
|
|
57
|
+
reservation = await reserveDestination(operationDestination);
|
|
91
58
|
temporaryRoot = await mkdtemp(join(operationParent, `.${basename(destination)}.gji-clone-`));
|
|
92
59
|
const temporaryDestination = join(temporaryRoot, basename(destination));
|
|
93
|
-
const copyDirectory = options.copyDirectory ??
|
|
94
|
-
(platformIsDarwin(platform)
|
|
95
|
-
? runNativeCloneDirectory
|
|
96
|
-
: async (source, target) => {
|
|
97
|
-
if (!strategy) {
|
|
98
|
-
throw new CloneUnsupportedError(`platform ${platform} has no CoW strategy`);
|
|
99
|
-
}
|
|
100
|
-
const runCommand = options.runCommand ?? runCloneCommand;
|
|
101
|
-
await runCommand("cp", strategy(source, target));
|
|
102
|
-
});
|
|
103
60
|
try {
|
|
104
|
-
await copyDirectory
|
|
61
|
+
await (options.copyDirectory ??
|
|
62
|
+
((sourcePath, targetPath) => copyDirectoryWithCow(sourcePath, targetPath, platform, options.runLinuxCommand)))(sourcePath, temporaryDestination);
|
|
105
63
|
}
|
|
106
64
|
catch (error) {
|
|
65
|
+
if (error instanceof CloneUnsupportedError)
|
|
66
|
+
throw error;
|
|
107
67
|
if (isUnsupportedCloneError(error)) {
|
|
108
68
|
throw new CloneUnsupportedError(toErrorMessage(error));
|
|
109
69
|
}
|
|
110
70
|
throw error;
|
|
111
71
|
}
|
|
112
|
-
await publishCloneContents(temporaryDestination, operationDestination,
|
|
113
|
-
|
|
72
|
+
await publishCloneContents(temporaryDestination, operationDestination, reservation.path, (entry) => publishedEntries.push(entry), options.copyFile ?? runForcedCloneFileCopy);
|
|
73
|
+
reservation = undefined;
|
|
114
74
|
}
|
|
115
75
|
finally {
|
|
116
76
|
stopLockHeartbeat();
|
|
117
|
-
if (
|
|
118
|
-
await cleanupReservedDestination(operationDestination,
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
if (temporaryRoot) {
|
|
122
|
-
await rm(temporaryRoot, { force: true, recursive: true });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// Cleanup is best effort and must not mask the clone result.
|
|
77
|
+
if (reservation) {
|
|
78
|
+
await cleanupReservedDestination(operationDestination, reservation, publishedEntries);
|
|
127
79
|
}
|
|
128
|
-
|
|
129
|
-
await
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// A stale lock is reclaimed on a later attempt.
|
|
80
|
+
if (temporaryRoot) {
|
|
81
|
+
await rm(temporaryRoot, { force: true, recursive: true }).catch(() => undefined);
|
|
133
82
|
}
|
|
83
|
+
await releaseCloneLock(cloneLock).catch(() => undefined);
|
|
134
84
|
}
|
|
135
|
-
|
|
136
|
-
? undefined
|
|
137
|
-
: await estimateCloneBytes(sourcePath);
|
|
138
|
-
return { bytes, ms: Date.now() - startedAt };
|
|
85
|
+
return cloneResult(sourcePath, startedAt, options.measureBytes);
|
|
139
86
|
}
|
|
140
87
|
finally {
|
|
141
88
|
await safeParent?.close().catch(() => undefined);
|
|
142
89
|
}
|
|
143
90
|
}
|
|
144
|
-
async function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
91
|
+
async function copyDirectoryWithCow(source, destination, platform, runLinuxCommand) {
|
|
92
|
+
if (platform === "darwin") {
|
|
93
|
+
await cp(source, destination, {
|
|
94
|
+
errorOnExist: true,
|
|
95
|
+
force: false,
|
|
96
|
+
mode: constants.COPYFILE_FICLONE_FORCE,
|
|
97
|
+
preserveTimestamps: true,
|
|
98
|
+
recursive: true,
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const strategy = cloneStrategy(platform);
|
|
103
|
+
if (!strategy) {
|
|
104
|
+
throw new CloneUnsupportedError(`platform ${platform} has no CoW strategy`);
|
|
105
|
+
}
|
|
106
|
+
await (runLinuxCommand ?? runCloneCommand)("cp", strategy(source, destination));
|
|
107
|
+
}
|
|
108
|
+
async function resolveProspectivePath(path) {
|
|
109
|
+
let current = resolve(path);
|
|
110
|
+
const missing = [];
|
|
111
|
+
while (true) {
|
|
112
|
+
try {
|
|
113
|
+
return join(await realpath(current), ...missing);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!isNotFoundError(error))
|
|
117
|
+
throw error;
|
|
118
|
+
const parent = dirname(current);
|
|
119
|
+
if (parent === current)
|
|
120
|
+
throw error;
|
|
121
|
+
missing.unshift(basename(current));
|
|
122
|
+
current = parent;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function cloneResult(source, startedAt, measureBytes) {
|
|
127
|
+
const bytes = measureBytes === false ? undefined : await estimateCloneBytes(source);
|
|
128
|
+
return { bytes, ms: Date.now() - startedAt };
|
|
152
129
|
}
|
|
153
130
|
export class CloneDestinationExistsError extends Error {
|
|
154
131
|
code = "GJI_CLONE_DESTINATION_EXISTS";
|
|
@@ -175,7 +152,7 @@ export function isCloneDestinationExistsError(error) {
|
|
|
175
152
|
return error instanceof CloneDestinationExistsError;
|
|
176
153
|
}
|
|
177
154
|
export function isCloneUnsupportedError(error) {
|
|
178
|
-
return
|
|
155
|
+
return error instanceof CloneUnsupportedError;
|
|
179
156
|
}
|
|
180
157
|
export function isCloneInProgressError(error) {
|
|
181
158
|
return error instanceof CloneInProgressError;
|
|
@@ -247,35 +224,31 @@ async function runCloneCommand(command, args) {
|
|
|
247
224
|
}
|
|
248
225
|
}
|
|
249
226
|
async function acquireCloneLock(lockPath, destination) {
|
|
227
|
+
const releaseGuard = await acquireCloneLockGuard(lockPath, destination);
|
|
228
|
+
try {
|
|
229
|
+
return await acquireCloneLockWithGuard(lockPath, destination);
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
await releaseGuard();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function acquireCloneLockWithGuard(lockPath, destination) {
|
|
250
236
|
const lockToken = randomUUID();
|
|
251
237
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
238
|
+
const published = await publishCloneLock(lockPath, lockToken);
|
|
239
|
+
if (published)
|
|
240
|
+
return published;
|
|
241
|
+
let staleLock;
|
|
252
242
|
try {
|
|
253
|
-
await
|
|
254
|
-
try {
|
|
255
|
-
await writeFile(join(lockPath, ownerFileName(lockToken)), `${lockToken}\n`, {
|
|
256
|
-
flag: "wx",
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
await rm(lockPath, { force: true, recursive: true });
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
return lockToken;
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
if (!isAlreadyExistsError(error))
|
|
267
|
-
throw error;
|
|
268
|
-
}
|
|
269
|
-
let lockStats;
|
|
270
|
-
try {
|
|
271
|
-
lockStats = await lstat(await lockFreshnessPath(lockPath));
|
|
243
|
+
staleLock = await inspectCloneLock(lockPath);
|
|
272
244
|
}
|
|
273
245
|
catch (error) {
|
|
274
246
|
if (isNotFoundError(error))
|
|
275
247
|
continue;
|
|
276
248
|
throw error;
|
|
277
249
|
}
|
|
278
|
-
if (
|
|
250
|
+
if (staleLock.kind === "invalid" ||
|
|
251
|
+
Date.now() - staleLock.mtimeMs < CLONE_LOCK_TTL_MS) {
|
|
279
252
|
throw new CloneInProgressError(destination);
|
|
280
253
|
}
|
|
281
254
|
const stalePath = `${lockPath}.stale-${randomUUID()}`;
|
|
@@ -288,27 +261,22 @@ async function acquireCloneLock(lockPath, destination) {
|
|
|
288
261
|
throw error;
|
|
289
262
|
}
|
|
290
263
|
try {
|
|
291
|
-
await
|
|
292
|
-
|
|
293
|
-
await
|
|
294
|
-
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
await rm(lockPath, { force: true, recursive: true });
|
|
299
|
-
throw error;
|
|
264
|
+
const replacement = await publishCloneLock(lockPath, lockToken);
|
|
265
|
+
if (!replacement) {
|
|
266
|
+
await cleanupStaleCloneLock(stalePath, staleLock);
|
|
267
|
+
continue;
|
|
300
268
|
}
|
|
301
269
|
try {
|
|
302
|
-
await
|
|
270
|
+
await cleanupStaleCloneLock(stalePath, staleLock);
|
|
303
271
|
}
|
|
304
272
|
catch (cleanupError) {
|
|
305
|
-
await
|
|
273
|
+
await releaseCloneLockWithGuard(replacement).catch(() => undefined);
|
|
306
274
|
throw cleanupError;
|
|
307
275
|
}
|
|
308
|
-
return
|
|
276
|
+
return replacement;
|
|
309
277
|
}
|
|
310
278
|
catch (error) {
|
|
311
|
-
await
|
|
279
|
+
await cleanupStaleCloneLock(stalePath, staleLock).catch(() => undefined);
|
|
312
280
|
if (isAlreadyExistsError(error))
|
|
313
281
|
continue;
|
|
314
282
|
throw error;
|
|
@@ -316,18 +284,172 @@ async function acquireCloneLock(lockPath, destination) {
|
|
|
316
284
|
}
|
|
317
285
|
throw new CloneInProgressError(destination);
|
|
318
286
|
}
|
|
319
|
-
async function
|
|
287
|
+
async function acquireCloneLockGuard(lockPath, destination) {
|
|
288
|
+
const guardPath = `${lockPath}.guard`;
|
|
289
|
+
const deadline = Date.now() + 5_000;
|
|
290
|
+
while (true) {
|
|
291
|
+
try {
|
|
292
|
+
await mkdir(guardPath, { mode: 0o700 });
|
|
293
|
+
const markerName = `${process.pid}-${randomUUID()}`;
|
|
294
|
+
const markerPath = join(guardPath, markerName);
|
|
295
|
+
try {
|
|
296
|
+
await writeFile(markerPath, `${markerName}\n`, {
|
|
297
|
+
flag: "wx",
|
|
298
|
+
mode: 0o600,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
await rmdir(guardPath).catch(() => undefined);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
return async () => {
|
|
306
|
+
await unlink(markerPath).catch((error) => {
|
|
307
|
+
if (!isNotFoundError(error))
|
|
308
|
+
throw error;
|
|
309
|
+
});
|
|
310
|
+
await rmdir(guardPath).catch((error) => {
|
|
311
|
+
if (!isNotFoundError(error))
|
|
312
|
+
throw error;
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (!isAlreadyExistsError(error))
|
|
318
|
+
throw error;
|
|
319
|
+
if (await reclaimAbandonedCloneGuard(guardPath))
|
|
320
|
+
continue;
|
|
321
|
+
if (Date.now() >= deadline) {
|
|
322
|
+
throw new CloneInProgressError(destination);
|
|
323
|
+
}
|
|
324
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function reclaimAbandonedCloneGuard(guardPath) {
|
|
329
|
+
let expectedMarker;
|
|
330
|
+
try {
|
|
331
|
+
const entries = await readdir(guardPath);
|
|
332
|
+
if (entries.length > 1)
|
|
333
|
+
return false;
|
|
334
|
+
expectedMarker = entries[0];
|
|
335
|
+
const freshnessPath = expectedMarker
|
|
336
|
+
? join(guardPath, expectedMarker)
|
|
337
|
+
: guardPath;
|
|
338
|
+
const stats = await lstat(freshnessPath);
|
|
339
|
+
if (Date.now() - stats.mtimeMs < CLONE_GUARD_TTL_MS)
|
|
340
|
+
return false;
|
|
341
|
+
if (expectedMarker) {
|
|
342
|
+
const ownerPid = Number(expectedMarker.split("-", 1)[0]);
|
|
343
|
+
if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0)
|
|
344
|
+
return false;
|
|
345
|
+
if (processIsAlive(ownerPid))
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
return isNotFoundError(error);
|
|
351
|
+
}
|
|
352
|
+
const stalePath = `${guardPath}.stale-${randomUUID()}`;
|
|
353
|
+
try {
|
|
354
|
+
await rename(guardPath, stalePath);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (isNotFoundError(error))
|
|
358
|
+
return true;
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
const movedEntries = await readdir(stalePath).catch(() => []);
|
|
362
|
+
if (movedEntries.length !== (expectedMarker ? 1 : 0) ||
|
|
363
|
+
(expectedMarker && movedEntries[0] !== expectedMarker)) {
|
|
364
|
+
await rename(stalePath, guardPath).catch(() => undefined);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (expectedMarker)
|
|
368
|
+
await unlink(join(stalePath, expectedMarker));
|
|
369
|
+
await rmdir(stalePath);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
function processIsAlive(pid) {
|
|
373
|
+
try {
|
|
374
|
+
process.kill(pid, 0);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
return error.code === "EPERM";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function publishCloneLock(lockPath, lockToken) {
|
|
382
|
+
try {
|
|
383
|
+
await mkdir(lockPath, { mode: 0o700 });
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
if (isAlreadyExistsError(error))
|
|
387
|
+
return undefined;
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
const markerPath = join(lockPath, lockToken);
|
|
391
|
+
try {
|
|
392
|
+
await writeFile(markerPath, `${lockToken}\n`, { flag: "wx", mode: 0o600 });
|
|
393
|
+
return {
|
|
394
|
+
lockPath,
|
|
395
|
+
markerName: lockToken,
|
|
396
|
+
markerPath,
|
|
397
|
+
token: lockToken,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
await rmdir(lockPath).catch(() => undefined);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function inspectCloneLock(lockPath) {
|
|
320
406
|
try {
|
|
407
|
+
const stats = await lstat(lockPath);
|
|
408
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
409
|
+
return { kind: "invalid", mtimeMs: stats.mtimeMs };
|
|
410
|
+
}
|
|
321
411
|
const entries = await readdir(lockPath);
|
|
322
|
-
|
|
323
|
-
|
|
412
|
+
if (entries.length === 0)
|
|
413
|
+
return { kind: "empty", mtimeMs: stats.mtimeMs };
|
|
414
|
+
if (entries.length !== 1) {
|
|
415
|
+
return { kind: "invalid", mtimeMs: stats.mtimeMs };
|
|
416
|
+
}
|
|
417
|
+
const markerName = entries[0];
|
|
418
|
+
const token = markerName?.startsWith("owner-")
|
|
419
|
+
? markerName.slice("owner-".length)
|
|
420
|
+
: markerName;
|
|
421
|
+
if (!token || !/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(token)) {
|
|
422
|
+
return { kind: "invalid", mtimeMs: stats.mtimeMs };
|
|
423
|
+
}
|
|
424
|
+
const markerPath = join(lockPath, markerName);
|
|
425
|
+
const markerStats = await lstat(markerPath);
|
|
426
|
+
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
427
|
+
return { kind: "invalid", mtimeMs: markerStats.mtimeMs };
|
|
428
|
+
}
|
|
429
|
+
if ((await readFile(markerPath, "utf8")).trim() !== token) {
|
|
430
|
+
return { kind: "invalid", mtimeMs: markerStats.mtimeMs };
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
kind: "owned",
|
|
434
|
+
lockPath,
|
|
435
|
+
markerName: markerName,
|
|
436
|
+
markerPath,
|
|
437
|
+
mtimeMs: markerStats.mtimeMs,
|
|
438
|
+
token,
|
|
439
|
+
};
|
|
324
440
|
}
|
|
325
441
|
catch (error) {
|
|
326
442
|
if (isNotFoundError(error))
|
|
327
443
|
throw error;
|
|
328
|
-
return
|
|
444
|
+
return { kind: "invalid", mtimeMs: Date.now() };
|
|
329
445
|
}
|
|
330
446
|
}
|
|
447
|
+
async function cleanupStaleCloneLock(lockPath, inspection) {
|
|
448
|
+
if (inspection.kind === "owned") {
|
|
449
|
+
await unlink(join(lockPath, inspection.markerName));
|
|
450
|
+
}
|
|
451
|
+
await rmdir(lockPath);
|
|
452
|
+
}
|
|
331
453
|
async function publishCloneContents(temporaryDestination, destination, reservationPath, onEntryPublished, copyFileEntry) {
|
|
332
454
|
const reservationName = basename(reservationPath);
|
|
333
455
|
const destinationEntries = await readdir(destination);
|
|
@@ -337,7 +459,7 @@ async function publishCloneContents(temporaryDestination, destination, reservati
|
|
|
337
459
|
}
|
|
338
460
|
const temporaryEntries = await readdir(temporaryDestination);
|
|
339
461
|
for (const entry of temporaryEntries) {
|
|
340
|
-
await publishCloneEntry(join(temporaryDestination, entry), join(destination, entry), () => onEntryPublished(entry), copyFileEntry);
|
|
462
|
+
await publishCloneEntry(join(temporaryDestination, entry), join(destination, entry), (entry) => onEntryPublished(entry), copyFileEntry);
|
|
341
463
|
}
|
|
342
464
|
await unlink(reservationPath);
|
|
343
465
|
}
|
|
@@ -353,7 +475,7 @@ async function publishCloneEntry(source, destination, onCreated, copyFileEntry)
|
|
|
353
475
|
}
|
|
354
476
|
throw error;
|
|
355
477
|
}
|
|
356
|
-
onCreated();
|
|
478
|
+
onCreated(await inspectOwnedCloneEntry(destination, "directory"));
|
|
357
479
|
for (const entry of await readdir(source)) {
|
|
358
480
|
await publishCloneEntry(join(source, entry), join(destination, entry), () => undefined, copyFileEntry);
|
|
359
481
|
}
|
|
@@ -370,7 +492,7 @@ async function publishCloneEntry(source, destination, onCreated, copyFileEntry)
|
|
|
370
492
|
}
|
|
371
493
|
throw error;
|
|
372
494
|
}
|
|
373
|
-
onCreated();
|
|
495
|
+
onCreated(await inspectOwnedCloneEntry(destination, "symlink"));
|
|
374
496
|
return;
|
|
375
497
|
}
|
|
376
498
|
if (!sourceStats.isFile()) {
|
|
@@ -388,7 +510,7 @@ async function publishCloneEntry(source, destination, onCreated, copyFileEntry)
|
|
|
388
510
|
}
|
|
389
511
|
throw error;
|
|
390
512
|
}
|
|
391
|
-
onCreated();
|
|
513
|
+
onCreated(await inspectOwnedCloneEntry(destination, "file"));
|
|
392
514
|
await copyCloneMetadata(sourceStats, destination);
|
|
393
515
|
}
|
|
394
516
|
async function runForcedCloneFileCopy(source, destination) {
|
|
@@ -418,26 +540,35 @@ async function reserveDestination(destination) {
|
|
|
418
540
|
await writeFile(reservationPath, "gji clone reservation\n", {
|
|
419
541
|
flag: "wx",
|
|
420
542
|
});
|
|
421
|
-
return reservationPath;
|
|
543
|
+
return await inspectOwnedCloneEntry(reservationPath, "file");
|
|
422
544
|
}
|
|
423
545
|
catch (error) {
|
|
424
546
|
await rmdir(destination).catch(() => undefined);
|
|
425
547
|
throw error;
|
|
426
548
|
}
|
|
427
549
|
}
|
|
428
|
-
async function cleanupReservedDestination(destination,
|
|
550
|
+
async function cleanupReservedDestination(destination, reservation, publishedEntries) {
|
|
551
|
+
for (const entry of [...publishedEntries, reservation].reverse()) {
|
|
552
|
+
await removeOwnedCloneEntry(entry);
|
|
553
|
+
}
|
|
554
|
+
await rmdir(destination).catch(() => undefined);
|
|
555
|
+
}
|
|
556
|
+
async function inspectOwnedCloneEntry(path, kind) {
|
|
557
|
+
const stats = await lstat(path, { bigint: true });
|
|
558
|
+
return { dev: stats.dev, ino: stats.ino, kind, path };
|
|
559
|
+
}
|
|
560
|
+
async function removeOwnedCloneEntry(entry) {
|
|
429
561
|
try {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
await
|
|
437
|
-
}
|
|
562
|
+
const current = await lstat(entry.path, { bigint: true });
|
|
563
|
+
if (current.dev !== entry.dev || current.ino !== entry.ino)
|
|
564
|
+
return;
|
|
565
|
+
if (entry.kind === "directory")
|
|
566
|
+
await rmdir(entry.path);
|
|
567
|
+
else
|
|
568
|
+
await unlink(entry.path);
|
|
438
569
|
}
|
|
439
570
|
catch {
|
|
440
|
-
//
|
|
571
|
+
// Missing, replaced, or non-empty entries are no longer exclusively ours.
|
|
441
572
|
}
|
|
442
573
|
}
|
|
443
574
|
async function destinationExists(path) {
|
|
@@ -451,37 +582,50 @@ async function destinationExists(path) {
|
|
|
451
582
|
throw error;
|
|
452
583
|
}
|
|
453
584
|
}
|
|
454
|
-
function startLockHeartbeat(
|
|
585
|
+
function startLockHeartbeat(lock) {
|
|
455
586
|
const timer = setInterval(() => {
|
|
456
|
-
void refreshCloneLock(
|
|
587
|
+
void refreshCloneLock(lock);
|
|
457
588
|
}, CLONE_LOCK_TTL_MS / 3);
|
|
458
589
|
timer.unref?.();
|
|
459
590
|
return () => clearInterval(timer);
|
|
460
591
|
}
|
|
461
|
-
async function refreshCloneLock(
|
|
592
|
+
async function refreshCloneLock(lock) {
|
|
462
593
|
try {
|
|
463
|
-
const ownerPath = join(lockPath, ownerFileName(lockToken));
|
|
464
|
-
await readFile(ownerPath, "utf8");
|
|
465
594
|
const now = new Date();
|
|
466
|
-
await utimes(
|
|
595
|
+
await utimes(lock.markerPath, now, now);
|
|
467
596
|
}
|
|
468
597
|
catch {
|
|
469
|
-
//
|
|
598
|
+
// A missing marker means this owner was already fenced out as stale.
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async function releaseCloneLock(lock) {
|
|
602
|
+
const releaseGuard = await acquireCloneLockGuard(lock.lockPath, lock.lockPath);
|
|
603
|
+
try {
|
|
604
|
+
await releaseCloneLockWithGuard(lock);
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
await releaseGuard();
|
|
470
608
|
}
|
|
471
609
|
}
|
|
472
|
-
async function
|
|
610
|
+
async function releaseCloneLockWithGuard(lock) {
|
|
473
611
|
try {
|
|
474
|
-
|
|
475
|
-
await unlink(ownerPath);
|
|
476
|
-
await rmdir(lockPath);
|
|
612
|
+
await unlink(join(lock.lockPath, lock.token));
|
|
477
613
|
}
|
|
478
614
|
catch (error) {
|
|
479
|
-
if (!isNotFoundError(error)
|
|
615
|
+
if (!isNotFoundError(error))
|
|
480
616
|
throw error;
|
|
481
617
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
618
|
+
try {
|
|
619
|
+
await rmdir(lock.lockPath);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
const code = "code" in error
|
|
623
|
+
? error.code
|
|
624
|
+
: undefined;
|
|
625
|
+
if (!isNotFoundError(error) && code !== "ENOTEMPTY" && code !== "EEXIST") {
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
485
629
|
}
|
|
486
630
|
function isUnsupportedCloneError(error) {
|
|
487
631
|
if (!(error instanceof Error))
|
|
@@ -490,16 +634,8 @@ function isUnsupportedCloneError(error) {
|
|
|
490
634
|
if (["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EXDEV"].includes(code ?? "")) {
|
|
491
635
|
return true;
|
|
492
636
|
}
|
|
493
|
-
return /
|
|
494
|
-
}
|
|
495
|
-
function isDirectoryNotEmptyError(error) {
|
|
496
|
-
return (error instanceof Error &&
|
|
497
|
-
"code" in error &&
|
|
498
|
-
error.code === "ENOTEMPTY");
|
|
637
|
+
return /reflink|unsupported|operation not supported|not supported|invalid cross-device|cross-device/iu.test(error.message);
|
|
499
638
|
}
|
|
500
639
|
function toErrorMessage(error) {
|
|
501
640
|
return error instanceof Error ? error.message : String(error);
|
|
502
641
|
}
|
|
503
|
-
function platformIsDarwin(platform) {
|
|
504
|
-
return platform === "darwin";
|
|
505
|
-
}
|