@dxos/lock-file 0.8.4-main.1da679c → 0.8.4-main.1f223c7
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/lib/node-esm/index.mjs +23 -103
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/lock-file.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -5
- package/src/lock-file.node.test.ts +43 -110
- package/src/lock-file.ts +24 -22
- package/dist/types/src/locker-subprocess.d.ts +0 -2
- package/dist/types/src/locker-subprocess.d.ts.map +0 -1
- package/dist/types/src/sys.d.ts +0 -10
- package/dist/types/src/sys.d.ts.map +0 -1
- package/src/locker-subprocess.ts +0 -22
- package/src/sys.ts +0 -84
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/lock-file",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.1f223c7",
|
|
4
4
|
"description": "Lock file .",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
"src"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"
|
|
28
|
-
"@dxos/
|
|
29
|
-
"@dxos/
|
|
27
|
+
"fs-ext": "2.0.0",
|
|
28
|
+
"@dxos/async": "0.8.4-main.1f223c7",
|
|
29
|
+
"@dxos/node-std": "0.8.4-main.1f223c7"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/fs-ext": "2.0.0"
|
|
30
33
|
},
|
|
31
|
-
"devDependencies": {},
|
|
32
34
|
"publishConfig": {
|
|
33
35
|
"access": "public"
|
|
34
36
|
}
|
|
@@ -4,27 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { existsSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
7
|
-
import { rm } from 'node:fs/promises';
|
|
8
|
-
import { tmpdir } from 'node:os';
|
|
9
7
|
import { join } from 'node:path';
|
|
10
8
|
|
|
11
|
-
import {
|
|
9
|
+
import { beforeAll, describe, expect, onTestFinished, test } from 'vitest';
|
|
12
10
|
|
|
13
11
|
import { Trigger } from '@dxos/async';
|
|
14
12
|
|
|
15
13
|
import { LockFile } from './lock-file';
|
|
16
14
|
|
|
17
15
|
const TEST_DIR = '/tmp/dxos/testing/lock-file';
|
|
18
|
-
const TEMP_TEST_DIR = join(tmpdir(), 'lock-file-test-' + Date.now());
|
|
19
16
|
|
|
20
17
|
describe('LockFile', () => {
|
|
21
18
|
beforeAll(() => {
|
|
22
19
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
23
|
-
mkdirSync(TEMP_TEST_DIR, { recursive: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterAll(async () => {
|
|
27
|
-
await rm(TEMP_TEST_DIR, { recursive: true, force: true });
|
|
28
20
|
});
|
|
29
21
|
|
|
30
22
|
test('basic', async () => {
|
|
@@ -47,65 +39,16 @@ describe('LockFile', () => {
|
|
|
47
39
|
});
|
|
48
40
|
|
|
49
41
|
const trigger = new Trigger();
|
|
50
|
-
const processHandle = spawn('
|
|
42
|
+
const processHandle = spawn('node', ['-e', `(${lockInProcess.toString()})(${JSON.stringify(filename)})`], {
|
|
51
43
|
stdio: 'pipe',
|
|
52
44
|
});
|
|
53
45
|
|
|
54
46
|
{
|
|
55
47
|
// Wait for process to start
|
|
56
|
-
processHandle.stdout.on('data', (data: Uint8Array) => {
|
|
57
|
-
process.stdout.write(data);
|
|
58
|
-
if (data.toString().trim().startsWith('#')) {
|
|
59
|
-
expect(data.toString().trim()).to.equal('# locked');
|
|
60
|
-
trigger.wake();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
processHandle.on('exit', (code) => {
|
|
64
|
-
trigger.throw(new Error(`Process exited pre with code ${code}`));
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await trigger.wait({ timeout: 5_000 });
|
|
69
|
-
|
|
70
|
-
await expect(LockFile.acquire(filename)).rejects.toBeInstanceOf(Error);
|
|
71
|
-
|
|
72
|
-
processHandle.stdin.write('close');
|
|
73
|
-
|
|
74
|
-
// Wait for process to be killed
|
|
75
|
-
await expect
|
|
76
|
-
.poll(async () => {
|
|
77
|
-
return await LockFile.isLocked(filename);
|
|
78
|
-
})
|
|
79
|
-
.toBe(false);
|
|
80
|
-
|
|
81
|
-
const handle = await LockFile.acquire(filename);
|
|
82
|
-
await LockFile.release(handle);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('released when process killed with SIGKILL', { timeout: 10_000 }, async () => {
|
|
86
|
-
const filename = join(TEST_DIR, `lock-${Math.random()}.lock`);
|
|
87
|
-
onTestFinished(() => {
|
|
88
|
-
if (existsSync(filename)) {
|
|
89
|
-
unlinkSync(filename);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
48
|
|
|
93
|
-
const trigger = new Trigger();
|
|
94
|
-
const processHandle = spawn('vite-node', [new URL('./locker-subprocess.ts', import.meta.url).pathname, filename], {
|
|
95
|
-
stdio: 'pipe',
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
// Wait for process to start
|
|
100
49
|
processHandle.stdout.on('data', (data: Uint8Array) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
expect(data.toString().trim()).to.equal('# locked');
|
|
104
|
-
trigger.wake();
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
processHandle.on('exit', (code) => {
|
|
108
|
-
trigger.throw(new Error(`Process exited pre with code ${code}`));
|
|
50
|
+
expect(data.toString().trim()).to.equal('locked');
|
|
51
|
+
trigger.wake();
|
|
109
52
|
});
|
|
110
53
|
}
|
|
111
54
|
|
|
@@ -113,7 +56,8 @@ describe('LockFile', () => {
|
|
|
113
56
|
|
|
114
57
|
await expect(LockFile.acquire(filename)).rejects.toBeInstanceOf(Error);
|
|
115
58
|
|
|
116
|
-
processHandle.
|
|
59
|
+
processHandle.stdin.write('close');
|
|
60
|
+
processHandle.kill();
|
|
117
61
|
|
|
118
62
|
// Wait for process to be killed
|
|
119
63
|
await expect
|
|
@@ -137,55 +81,44 @@ describe('LockFile', () => {
|
|
|
137
81
|
expect(await LockFile.isLocked(filename)).to.be.false;
|
|
138
82
|
}
|
|
139
83
|
});
|
|
84
|
+
});
|
|
140
85
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const nonExistent = join(TEMP_TEST_DIR, 'non-existent.lock');
|
|
169
|
-
await expect(LockFile.isLocked(nonExistent)).resolves.toBe(false);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('should handle concurrent lock attempts', async () => {
|
|
173
|
-
const lockFile = join(TEMP_TEST_DIR, 'test3.lock');
|
|
174
|
-
const handle = await LockFile.acquire(lockFile);
|
|
175
|
-
|
|
176
|
-
// Multiple attempts to acquire should all fail
|
|
177
|
-
const attempts = Array(5)
|
|
178
|
-
.fill(0)
|
|
179
|
-
.map(() => LockFile.acquire(lockFile).catch((err) => err));
|
|
180
|
-
|
|
181
|
-
const results = await Promise.all(attempts);
|
|
182
|
-
|
|
183
|
-
// All attempts should have failed
|
|
184
|
-
results.forEach((result) => {
|
|
185
|
-
expect(result).toBeInstanceOf(Error);
|
|
186
|
-
expect(result.message).toContain('flock failed');
|
|
86
|
+
// NOTE: Self-contained so when function.toString is called the code runs.
|
|
87
|
+
const lockInProcess = (filename: string) => {
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
89
|
+
const { open } = require('node:fs/promises');
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
91
|
+
const { constants } = require('node:fs');
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
93
|
+
const { flock } = require('fs-ext');
|
|
94
|
+
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
let fileHandle;
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
open(filename, constants.O_CREAT).then((handle) => {
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
flock(handle.fd, 'exnb', (err) => {
|
|
104
|
+
if (err) {
|
|
105
|
+
handle.close();
|
|
106
|
+
console.error(err);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
fileHandle = handle;
|
|
110
|
+
console.log('locked');
|
|
111
|
+
// Hang
|
|
112
|
+
setTimeout(() => {}, 1_000_000);
|
|
187
113
|
});
|
|
114
|
+
});
|
|
188
115
|
|
|
189
|
-
|
|
116
|
+
// Close file handle on stdin close.
|
|
117
|
+
process.stdin.on('data', (data) => {
|
|
118
|
+
if (data.toString().trim() === 'close') {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
120
|
+
// @ts-ignore
|
|
121
|
+
fileHandle.close();
|
|
122
|
+
}
|
|
190
123
|
});
|
|
191
|
-
}
|
|
124
|
+
};
|
package/src/lock-file.ts
CHANGED
|
@@ -5,34 +5,35 @@
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { type FileHandle, constants, open } from 'node:fs/promises';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const sys = new LockfileSys();
|
|
8
|
+
import { flock } from 'fs-ext';
|
|
11
9
|
|
|
12
10
|
export class LockFile {
|
|
13
11
|
static async acquire(filename: string): Promise<FileHandle> {
|
|
14
|
-
await
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
12
|
+
const handle = await open(filename, constants.O_CREAT);
|
|
13
|
+
await new Promise<void>((resolve, reject) => {
|
|
14
|
+
flock(handle.fd, 'exnb', async (err) => {
|
|
15
|
+
if (err) {
|
|
16
|
+
reject(err);
|
|
17
|
+
await handle.close();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
resolve();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
return handle;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
static async release(handle: FileHandle): Promise<void> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
await new Promise<void>((resolve, reject) => {
|
|
28
|
+
flock(handle.fd, 'un', (err) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
reject(err);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
await handle.close();
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
static async isLocked(filename: string): Promise<boolean> {
|
|
@@ -42,6 +43,7 @@ export class LockFile {
|
|
|
42
43
|
try {
|
|
43
44
|
const handle = await LockFile.acquire(filename);
|
|
44
45
|
await LockFile.release(handle);
|
|
46
|
+
|
|
45
47
|
return false;
|
|
46
48
|
} catch (e) {
|
|
47
49
|
return true;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"locker-subprocess.d.ts","sourceRoot":"","sources":["../../../src/locker-subprocess.ts"],"names":[],"mappings":""}
|
package/dist/types/src/sys.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sys.d.ts","sourceRoot":"","sources":["../../../src/sys.ts"],"names":[],"mappings":"AAcA,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAA8B;IAE3C,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,YAAY,CAAoC;IAElD,IAAI;YAII,QAAQ;IAetB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CA2CpC"}
|
package/src/locker-subprocess.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { LockFile } from './lock-file';
|
|
6
|
-
|
|
7
|
-
const filename = process.argv[2];
|
|
8
|
-
|
|
9
|
-
console.log('will lock');
|
|
10
|
-
|
|
11
|
-
const handle = await LockFile.acquire(filename);
|
|
12
|
-
// parents looks for # symbol in the output to know when the lock is acquired
|
|
13
|
-
console.log('# locked');
|
|
14
|
-
|
|
15
|
-
// Close file handle on stdin close.
|
|
16
|
-
process.stdin.on('data', async (data) => {
|
|
17
|
-
if (data.toString().trim() === 'close') {
|
|
18
|
-
console.log('will unlock');
|
|
19
|
-
await LockFile.release(handle);
|
|
20
|
-
console.log('unlocked');
|
|
21
|
-
}
|
|
22
|
-
});
|
package/src/sys.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { platform } from 'node:os';
|
|
6
|
-
|
|
7
|
-
import type * as koffi from 'koffi';
|
|
8
|
-
|
|
9
|
-
// flock constants
|
|
10
|
-
const LOCK_SH = 1; // Shared lock
|
|
11
|
-
const LOCK_EX = 2; // Exclusive lock
|
|
12
|
-
const LOCK_NB = 4; // Non-blocking
|
|
13
|
-
const LOCK_UN = 8; // Unlock
|
|
14
|
-
|
|
15
|
-
export class LockfileSys {
|
|
16
|
-
private _init: Promise<void> | null = null;
|
|
17
|
-
|
|
18
|
-
private _koffi: typeof koffi | null = null;
|
|
19
|
-
private _libc: koffi.IKoffiLib | null = null;
|
|
20
|
-
private _flockNative: koffi.KoffiFunction | null = null;
|
|
21
|
-
|
|
22
|
-
async init() {
|
|
23
|
-
await (this._init ??= this._runInit());
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private async _runInit() {
|
|
27
|
-
this._koffi = await import('koffi');
|
|
28
|
-
switch (platform()) {
|
|
29
|
-
case 'darwin':
|
|
30
|
-
this._libc = this._koffi.load('libc.dylib');
|
|
31
|
-
break;
|
|
32
|
-
case 'linux':
|
|
33
|
-
this._libc = this._koffi.load('libc.so.6');
|
|
34
|
-
break;
|
|
35
|
-
default:
|
|
36
|
-
throw new Error(`Unsupported platform: ${platform()}`);
|
|
37
|
-
}
|
|
38
|
-
this._flockNative = this._libc.func('flock', 'int', ['int', 'int']);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
flock(fd: number, operation: string) {
|
|
42
|
-
if (!this._flockNative) {
|
|
43
|
-
throw new Error('flock not initialized');
|
|
44
|
-
}
|
|
45
|
-
let op = 0;
|
|
46
|
-
|
|
47
|
-
switch (operation) {
|
|
48
|
-
case 'ex':
|
|
49
|
-
op = LOCK_EX;
|
|
50
|
-
break;
|
|
51
|
-
case 'exnb':
|
|
52
|
-
op = LOCK_EX | LOCK_NB;
|
|
53
|
-
break;
|
|
54
|
-
case 'sh':
|
|
55
|
-
op = LOCK_SH;
|
|
56
|
-
break;
|
|
57
|
-
case 'shnb':
|
|
58
|
-
op = LOCK_SH | LOCK_NB;
|
|
59
|
-
break;
|
|
60
|
-
case 'un':
|
|
61
|
-
op = LOCK_UN;
|
|
62
|
-
break;
|
|
63
|
-
default:
|
|
64
|
-
throw new Error(`Invalid flock operation: ${operation}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const result = this._flockNative!(fd, op);
|
|
68
|
-
|
|
69
|
-
if (result !== 0) {
|
|
70
|
-
// Get the errno to provide a more meaningful error
|
|
71
|
-
const errno = this._koffi!.errno();
|
|
72
|
-
const errorMessages: { [key: number]: string } = {
|
|
73
|
-
11: 'Resource temporarily unavailable (EAGAIN/EWOULDBLOCK)',
|
|
74
|
-
13: 'Permission denied (EACCES)',
|
|
75
|
-
22: 'Invalid argument (EINVAL)',
|
|
76
|
-
9: 'Bad file descriptor (EBADF)',
|
|
77
|
-
35: 'Resource temporarily unavailable (EAGAIN on macOS)',
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const errorMessage = errorMessages[errno] || `Unknown error (errno: ${errno})`;
|
|
81
|
-
throw new Error(`flock failed: ${errorMessage}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|