aether-colony 3.1.5 → 3.1.16
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/.claude/commands/ant/archaeology.md +12 -0
- package/.claude/commands/ant/build.md +382 -319
- package/.claude/commands/ant/chaos.md +23 -1
- package/.claude/commands/ant/colonize.md +147 -87
- package/.claude/commands/ant/continue.md +213 -23
- package/.claude/commands/ant/council.md +22 -0
- package/.claude/commands/ant/dream.md +18 -0
- package/.claude/commands/ant/entomb.md +178 -6
- package/.claude/commands/ant/init.md +87 -13
- package/.claude/commands/ant/lay-eggs.md +45 -5
- package/.claude/commands/ant/oracle.md +82 -9
- package/.claude/commands/ant/organize.md +2 -2
- package/.claude/commands/ant/pause-colony.md +86 -28
- package/.claude/commands/ant/phase.md +26 -0
- package/.claude/commands/ant/plan.md +204 -111
- package/.claude/commands/ant/resume-colony.md +23 -1
- package/.claude/commands/ant/resume.md +159 -0
- package/.claude/commands/ant/seal.md +177 -3
- package/.claude/commands/ant/swarm.md +78 -97
- package/.claude/commands/ant/verify-castes.md +7 -7
- package/.claude/commands/ant/watch.md +17 -0
- package/.opencode/agents/aether-ambassador.md +97 -0
- package/.opencode/agents/aether-archaeologist.md +91 -0
- package/.opencode/agents/aether-architect.md +66 -0
- package/.opencode/agents/aether-auditor.md +111 -0
- package/.opencode/agents/aether-builder.md +28 -10
- package/.opencode/agents/aether-chaos.md +98 -0
- package/.opencode/agents/aether-chronicler.md +80 -0
- package/.opencode/agents/aether-gatekeeper.md +107 -0
- package/.opencode/agents/aether-guardian.md +107 -0
- package/.opencode/agents/aether-includer.md +108 -0
- package/.opencode/agents/aether-keeper.md +106 -0
- package/.opencode/agents/aether-measurer.md +119 -0
- package/.opencode/agents/aether-probe.md +91 -0
- package/.opencode/agents/aether-queen.md +72 -19
- package/.opencode/agents/aether-route-setter.md +85 -0
- package/.opencode/agents/aether-sage.md +98 -0
- package/.opencode/agents/aether-scout.md +33 -15
- package/.opencode/agents/aether-surveyor-disciplines.md +334 -0
- package/.opencode/agents/aether-surveyor-nest.md +272 -0
- package/.opencode/agents/aether-surveyor-pathogens.md +209 -0
- package/.opencode/agents/aether-surveyor-provisions.md +277 -0
- package/.opencode/agents/aether-tracker.md +91 -0
- package/.opencode/agents/aether-watcher.md +30 -12
- package/.opencode/agents/aether-weaver.md +87 -0
- package/.opencode/agents/workers.md +1034 -0
- package/.opencode/commands/ant/archaeology.md +44 -26
- package/.opencode/commands/ant/build.md +326 -294
- package/.opencode/commands/ant/chaos.md +32 -4
- package/.opencode/commands/ant/colonize.md +119 -93
- package/.opencode/commands/ant/continue.md +98 -10
- package/.opencode/commands/ant/council.md +28 -0
- package/.opencode/commands/ant/dream.md +24 -0
- package/.opencode/commands/ant/entomb.md +73 -1
- package/.opencode/commands/ant/feedback.md +8 -2
- package/.opencode/commands/ant/flag.md +9 -3
- package/.opencode/commands/ant/flags.md +8 -2
- package/.opencode/commands/ant/focus.md +8 -2
- package/.opencode/commands/ant/help.md +12 -0
- package/.opencode/commands/ant/init.md +49 -4
- package/.opencode/commands/ant/lay-eggs.md +30 -2
- package/.opencode/commands/ant/oracle.md +39 -7
- package/.opencode/commands/ant/organize.md +8 -2
- package/.opencode/commands/ant/pause-colony.md +54 -1
- package/.opencode/commands/ant/phase.md +36 -4
- package/.opencode/commands/ant/plan.md +224 -116
- package/.opencode/commands/ant/redirect.md +8 -2
- package/.opencode/commands/ant/resume-colony.md +51 -26
- package/.opencode/commands/ant/seal.md +76 -0
- package/.opencode/commands/ant/status.md +50 -20
- package/.opencode/commands/ant/swarm.md +108 -104
- package/.opencode/commands/ant/tunnels.md +107 -2
- package/CHANGELOG.md +16 -0
- package/README.md +199 -86
- package/bin/cli.js +142 -25
- package/bin/generate-commands.sh +100 -16
- package/bin/lib/caste-colors.js +5 -5
- package/bin/lib/errors.js +16 -0
- package/bin/lib/file-lock.js +279 -44
- package/bin/lib/state-sync.js +206 -23
- package/bin/lib/update-transaction.js +206 -24
- package/bin/sync-to-runtime.sh +138 -0
- package/package.json +2 -2
- package/runtime/CONTEXT.md +160 -0
- package/runtime/aether-utils.sh +1421 -55
- package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +1343 -0
- package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +2642 -0
- package/runtime/docs/PHEROMONE-INJECTION.md +240 -0
- package/runtime/docs/PHEROMONE-INTEGRATION.md +192 -0
- package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +426 -0
- package/runtime/docs/README.md +94 -0
- package/runtime/docs/VISUAL-OUTPUT-SPEC.md +219 -0
- package/runtime/docs/biological-reference.md +272 -0
- package/runtime/docs/codebase-review.md +399 -0
- package/runtime/docs/command-sync.md +164 -0
- package/runtime/docs/implementation-learnings.md +89 -0
- package/runtime/docs/known-issues.md +217 -0
- package/runtime/docs/namespace.md +148 -0
- package/runtime/docs/planning-discipline.md +159 -0
- package/runtime/exchange/pheromone-xml.sh +574 -0
- package/runtime/exchange/registry-xml.sh +269 -0
- package/runtime/exchange/wisdom-xml.sh +312 -0
- package/runtime/lib/queen-utils.sh +729 -0
- package/runtime/model-profiles.yaml +100 -0
- package/runtime/recover.sh +136 -0
- package/runtime/schemas/aether-types.xsd +255 -0
- package/runtime/schemas/colony-registry.xsd +309 -0
- package/runtime/schemas/pheromone.xsd +163 -0
- package/runtime/schemas/prompt.xsd +416 -0
- package/runtime/schemas/queen-wisdom.xsd +325 -0
- package/runtime/schemas/worker-priming.xsd +276 -0
- package/runtime/templates/QUEEN.md.template +79 -0
- package/runtime/utils/atomic-write.sh +5 -5
- package/runtime/utils/chamber-utils.sh +6 -3
- package/runtime/utils/error-handler.sh +200 -0
- package/runtime/utils/queen-to-md.xsl +395 -0
- package/runtime/utils/spawn-tree.sh +428 -0
- package/runtime/utils/spawn-with-model.sh +56 -0
- package/runtime/utils/state-loader.sh +215 -0
- package/runtime/utils/swarm-display.sh +5 -5
- package/runtime/utils/watch-spawn-tree.sh +90 -22
- package/runtime/utils/xml-compose.sh +247 -0
- package/runtime/utils/xml-core.sh +186 -0
- package/runtime/utils/xml-utils.sh +2196 -0
- package/runtime/verification-loop.md +1 -1
- package/runtime/workers-new-castes.md +516 -0
- package/runtime/workers.md +18 -6
- package/.aether/visualizations/anthill-stages/brood-stable.txt +0 -26
- package/.aether/visualizations/anthill-stages/crowned-anthill.txt +0 -30
- package/.aether/visualizations/anthill-stages/first-mound.txt +0 -18
- package/.aether/visualizations/anthill-stages/open-chambers.txt +0 -24
- package/.aether/visualizations/anthill-stages/sealed-chambers.txt +0 -28
- package/.aether/visualizations/anthill-stages/ventilated-nest.txt +0 -27
package/bin/lib/file-lock.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const { FileSystemError } = require('./errors');
|
|
13
|
+
const { FileSystemError, ConfigurationError } = require('./errors');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Default configuration for lock behavior
|
|
@@ -20,8 +20,15 @@ const DEFAULT_OPTIONS = {
|
|
|
20
20
|
timeout: 5000, // 5 seconds total timeout
|
|
21
21
|
retryInterval: 50, // 50ms between retries
|
|
22
22
|
maxRetries: 100, // Total 5 seconds max wait
|
|
23
|
+
maxLockAge: 5 * 60 * 1000, // 5 minutes - configurable stale lock threshold
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Module-level set of registered cleanup functions (PLAN-006 fix #4)
|
|
28
|
+
* Prevents duplicate cleanup handler registration across multiple FileLock instances
|
|
29
|
+
*/
|
|
30
|
+
const registeredCleanups = new Set();
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* FileLock class for exclusive file locking with stale detection
|
|
27
34
|
*
|
|
@@ -37,9 +44,51 @@ class FileLock {
|
|
|
37
44
|
* @param {number} options.timeout - Total timeout in milliseconds (default: 5000)
|
|
38
45
|
* @param {number} options.retryInterval - Milliseconds between retries (default: 50)
|
|
39
46
|
* @param {number} options.maxRetries - Maximum retry attempts (default: 100)
|
|
47
|
+
* @param {number} options.maxLockAge - Maximum lock age in ms before considered stale (default: 300000)
|
|
40
48
|
*/
|
|
41
49
|
constructor(options = {}) {
|
|
42
50
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
51
|
+
|
|
52
|
+
// Validate lockDir (PLAN-006 fix #2)
|
|
53
|
+
if (!this.options.lockDir || typeof this.options.lockDir !== 'string') {
|
|
54
|
+
throw new ConfigurationError(
|
|
55
|
+
'lockDir must be a non-empty string',
|
|
56
|
+
{ lockDir: this.options.lockDir }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate timeout (PLAN-006 fix #5)
|
|
61
|
+
if (typeof this.options.timeout !== 'number' || this.options.timeout < 0) {
|
|
62
|
+
throw new ConfigurationError(
|
|
63
|
+
'timeout must be a non-negative number',
|
|
64
|
+
{ timeout: this.options.timeout }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate retryInterval
|
|
69
|
+
if (typeof this.options.retryInterval !== 'number' || this.options.retryInterval < 0) {
|
|
70
|
+
throw new ConfigurationError(
|
|
71
|
+
'retryInterval must be a non-negative number',
|
|
72
|
+
{ retryInterval: this.options.retryInterval }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate maxRetries
|
|
77
|
+
if (typeof this.options.maxRetries !== 'number' || this.options.maxRetries < 0) {
|
|
78
|
+
throw new ConfigurationError(
|
|
79
|
+
'maxRetries must be a non-negative number',
|
|
80
|
+
{ maxRetries: this.options.maxRetries }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate maxLockAge (PLAN-007 Fix 1)
|
|
85
|
+
if (typeof this.options.maxLockAge !== 'number' || this.options.maxLockAge < 0) {
|
|
86
|
+
throw new ConfigurationError(
|
|
87
|
+
'maxLockAge must be a non-negative number',
|
|
88
|
+
{ maxLockAge: this.options.maxLockAge }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
43
92
|
this.currentLock = null;
|
|
44
93
|
this.currentPidFile = null;
|
|
45
94
|
|
|
@@ -69,9 +118,20 @@ class FileLock {
|
|
|
69
118
|
|
|
70
119
|
/**
|
|
71
120
|
* Register process cleanup handlers to ensure locks are released on exit
|
|
121
|
+
* Uses module-level tracking to prevent duplicate registrations (PLAN-006 fix #4)
|
|
72
122
|
* @private
|
|
73
123
|
*/
|
|
74
124
|
_registerCleanupHandlers() {
|
|
125
|
+
// Create unique identifier for this cleanup based on lock directory
|
|
126
|
+
const cleanupId = `filelock-${this.options.lockDir}`;
|
|
127
|
+
|
|
128
|
+
// Only register if not already registered
|
|
129
|
+
if (registeredCleanups.has(cleanupId)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
registeredCleanups.add(cleanupId);
|
|
134
|
+
|
|
75
135
|
const cleanup = () => {
|
|
76
136
|
this.release();
|
|
77
137
|
};
|
|
@@ -131,50 +191,111 @@ class FileLock {
|
|
|
131
191
|
}
|
|
132
192
|
}
|
|
133
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Safely unlink a file, ignoring ENOENT errors
|
|
196
|
+
*
|
|
197
|
+
* @param {string} filePath - Path to file to unlink
|
|
198
|
+
* @private
|
|
199
|
+
*/
|
|
200
|
+
_safeUnlink(filePath) {
|
|
201
|
+
try {
|
|
202
|
+
fs.unlinkSync(filePath);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error.code !== 'ENOENT') {
|
|
205
|
+
// Log but don't throw - we're cleaning up
|
|
206
|
+
console.warn(`Warning: Failed to clean up ${filePath}: ${error.message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
134
211
|
/**
|
|
135
212
|
* Clean up stale lock files
|
|
136
213
|
*
|
|
214
|
+
* Checks both PID file and lock file for PID (handles crash scenarios
|
|
215
|
+
* where only one file was created). Also checks lock age to handle
|
|
216
|
+
* PID reuse race condition.
|
|
217
|
+
*
|
|
137
218
|
* @param {string} lockFile - Path to lock file
|
|
138
219
|
* @param {string} pidFile - Path to PID file
|
|
220
|
+
* @returns {boolean} True if lock was cleaned up, false if still held
|
|
139
221
|
* @private
|
|
140
222
|
*/
|
|
141
223
|
_cleanupStaleLock(lockFile, pidFile) {
|
|
224
|
+
// Maximum lock age before considering stale (configurable, default 5 minutes)
|
|
225
|
+
// This handles PID reuse race condition
|
|
226
|
+
const maxLockAgeMs = this.options.maxLockAge;
|
|
227
|
+
|
|
142
228
|
try {
|
|
143
|
-
//
|
|
144
|
-
if (fs.existsSync(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
229
|
+
// Check lock file age first (handles PID reuse)
|
|
230
|
+
if (fs.existsSync(lockFile)) {
|
|
231
|
+
try {
|
|
232
|
+
const stat = fs.statSync(lockFile);
|
|
233
|
+
const lockAge = Date.now() - stat.mtimeMs;
|
|
234
|
+
|
|
235
|
+
if (lockAge > maxLockAgeMs) {
|
|
236
|
+
// Lock is old enough to be considered stale regardless of PID
|
|
237
|
+
this._safeUnlink(lockFile);
|
|
238
|
+
this._safeUnlink(pidFile);
|
|
239
|
+
return true;
|
|
153
240
|
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Cannot stat, proceed with PID check
|
|
154
243
|
}
|
|
155
244
|
}
|
|
156
245
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
246
|
+
let pid = null;
|
|
247
|
+
|
|
248
|
+
// Try to read PID from PID file first
|
|
249
|
+
if (fs.existsSync(pidFile)) {
|
|
250
|
+
try {
|
|
251
|
+
const pidData = fs.readFileSync(pidFile, 'utf8').trim();
|
|
252
|
+
|
|
253
|
+
// Validate PID is a positive integer (PLAN-006 fix #3)
|
|
254
|
+
if (!/^\d+$/.test(pidData)) {
|
|
255
|
+
// PID file contains invalid data - clean it up
|
|
256
|
+
this._safeUnlink(lockFile);
|
|
257
|
+
this._safeUnlink(pidFile);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
pid = parseInt(pidData, 10);
|
|
262
|
+
} catch (readError) {
|
|
263
|
+
// PID file unreadable - will clean up
|
|
163
264
|
}
|
|
164
265
|
}
|
|
165
266
|
|
|
166
|
-
try
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
267
|
+
// If no valid PID from PID file, try lock file itself
|
|
268
|
+
if ((pid === null || isNaN(pid)) && fs.existsSync(lockFile)) {
|
|
269
|
+
try {
|
|
270
|
+
const lockData = fs.readFileSync(lockFile, 'utf8').trim();
|
|
271
|
+
|
|
272
|
+
// Validate PID is a positive integer
|
|
273
|
+
if (!/^\d+$/.test(lockData)) {
|
|
274
|
+
// Lock file contains invalid data - clean it up
|
|
275
|
+
this._safeUnlink(lockFile);
|
|
276
|
+
this._safeUnlink(pidFile);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
pid = parseInt(lockData, 10);
|
|
281
|
+
} catch (readError) {
|
|
282
|
+
// Lock file unreadable - will clean up
|
|
171
283
|
}
|
|
172
284
|
}
|
|
173
285
|
|
|
286
|
+
// Check if process is running
|
|
287
|
+
if (!isNaN(pid) && this._isProcessRunning(pid)) {
|
|
288
|
+
// Process is running, lock is valid
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Either no valid PID or process not running - clean up stale lock
|
|
293
|
+
this._safeUnlink(lockFile);
|
|
294
|
+
this._safeUnlink(pidFile);
|
|
295
|
+
|
|
174
296
|
return true;
|
|
175
297
|
} catch (error) {
|
|
176
298
|
if (error.code === 'ENOENT') {
|
|
177
|
-
// Lock file doesn't exist, consider it cleaned
|
|
178
299
|
return true;
|
|
179
300
|
}
|
|
180
301
|
throw new FileSystemError(
|
|
@@ -187,39 +308,73 @@ class FileLock {
|
|
|
187
308
|
/**
|
|
188
309
|
* Attempt to acquire a lock atomically
|
|
189
310
|
*
|
|
311
|
+
* Uses PID-file-first ordering for crash recovery:
|
|
312
|
+
* 1. Write PID file first (if this fails, no lock file created)
|
|
313
|
+
* 2. Create lock file atomically
|
|
314
|
+
* 3. On failure, clean up both files
|
|
315
|
+
*
|
|
190
316
|
* @param {string} lockFile - Path to lock file
|
|
191
317
|
* @param {string} pidFile - Path to PID file
|
|
192
318
|
* @returns {boolean} True if lock acquired, false otherwise
|
|
319
|
+
* @throws {FileSystemError} On unexpected filesystem errors
|
|
193
320
|
* @private
|
|
194
321
|
*/
|
|
195
322
|
_tryAcquire(lockFile, pidFile) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const fd = fs.openSync(lockFile, 'wx');
|
|
323
|
+
let pidFileCreated = false;
|
|
324
|
+
let lockFileCreated = false;
|
|
199
325
|
|
|
326
|
+
try {
|
|
327
|
+
// Step 1: Write PID file first (easier to clean up if lock fails)
|
|
200
328
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
329
|
+
fs.writeFileSync(pidFile, process.pid.toString(), 'utf8');
|
|
330
|
+
pidFileCreated = true;
|
|
331
|
+
} catch (pidError) {
|
|
332
|
+
// Cannot write PID file - cannot proceed
|
|
333
|
+
throw new FileSystemError(
|
|
334
|
+
`Failed to write PID file: ${pidFile}`,
|
|
335
|
+
{ error: pidError.message, code: pidError.code }
|
|
336
|
+
);
|
|
206
337
|
}
|
|
207
338
|
|
|
208
|
-
//
|
|
209
|
-
|
|
339
|
+
// Step 2: Create lock file atomically
|
|
340
|
+
try {
|
|
341
|
+
const fd = fs.openSync(lockFile, 'wx');
|
|
342
|
+
lockFileCreated = true;
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// Write PID to lock file as well (for redundancy)
|
|
346
|
+
fs.writeFileSync(fd, process.pid.toString(), 'utf8');
|
|
347
|
+
} finally {
|
|
348
|
+
fs.closeSync(fd);
|
|
349
|
+
}
|
|
350
|
+
} catch (lockError) {
|
|
351
|
+
if (lockError.code === 'EEXIST') {
|
|
352
|
+
// Lock file exists - clean up our PID file
|
|
353
|
+
this._safeUnlink(pidFile);
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
throw lockError;
|
|
357
|
+
}
|
|
210
358
|
|
|
211
|
-
// Track current lock
|
|
359
|
+
// Step 3: Track current lock (only after both files created)
|
|
212
360
|
this.currentLock = lockFile;
|
|
213
361
|
this.currentPidFile = pidFile;
|
|
214
362
|
|
|
215
363
|
return true;
|
|
364
|
+
|
|
216
365
|
} catch (error) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
366
|
+
// Clean up on any failure
|
|
367
|
+
if (lockFileCreated) {
|
|
368
|
+
this._safeUnlink(lockFile);
|
|
369
|
+
}
|
|
370
|
+
if (pidFileCreated) {
|
|
371
|
+
this._safeUnlink(pidFile);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (error instanceof FileSystemError) {
|
|
375
|
+
throw error;
|
|
220
376
|
}
|
|
221
377
|
|
|
222
|
-
// Unexpected error
|
|
223
378
|
throw new FileSystemError(
|
|
224
379
|
`Failed to acquire lock: ${lockFile}`,
|
|
225
380
|
{ error: error.message, code: error.code }
|
|
@@ -228,7 +383,10 @@ class FileLock {
|
|
|
228
383
|
}
|
|
229
384
|
|
|
230
385
|
/**
|
|
231
|
-
* Acquire an exclusive lock on a file
|
|
386
|
+
* Acquire an exclusive lock on a file (SYNCHRONOUS - may block event loop)
|
|
387
|
+
*
|
|
388
|
+
* WARNING: This method uses a busy-wait loop that blocks the Node.js event loop
|
|
389
|
+
* during retry intervals. For non-blocking operation, use acquireAsync() instead.
|
|
232
390
|
*
|
|
233
391
|
* @param {string} filePath - Path to the file to lock
|
|
234
392
|
* @returns {boolean} True if lock acquired, false on timeout
|
|
@@ -281,25 +439,26 @@ class FileLock {
|
|
|
281
439
|
*
|
|
282
440
|
* This method is idempotent - safe to call multiple times.
|
|
283
441
|
*
|
|
284
|
-
* @returns {boolean} True if
|
|
442
|
+
* @returns {boolean} True if lock was fully released, false if no lock was held
|
|
443
|
+
* or if deletion failed (check console.warn for details)
|
|
285
444
|
*/
|
|
286
445
|
release() {
|
|
287
446
|
if (!this.currentLock) {
|
|
288
447
|
return false;
|
|
289
448
|
}
|
|
290
449
|
|
|
291
|
-
let
|
|
450
|
+
let success = true;
|
|
292
451
|
|
|
293
452
|
// Delete lock file
|
|
294
453
|
try {
|
|
295
454
|
if (fs.existsSync(this.currentLock)) {
|
|
296
455
|
fs.unlinkSync(this.currentLock);
|
|
297
|
-
released = true;
|
|
298
456
|
}
|
|
299
457
|
} catch (error) {
|
|
300
458
|
if (error.code !== 'ENOENT') {
|
|
301
459
|
// Log but don't throw - we're cleaning up
|
|
302
460
|
console.warn(`Warning: Failed to remove lock file: ${error.message}`);
|
|
461
|
+
success = false;
|
|
303
462
|
}
|
|
304
463
|
}
|
|
305
464
|
|
|
@@ -312,6 +471,7 @@ class FileLock {
|
|
|
312
471
|
} catch (error) {
|
|
313
472
|
if (error.code !== 'ENOENT') {
|
|
314
473
|
console.warn(`Warning: Failed to remove PID file: ${error.message}`);
|
|
474
|
+
success = false;
|
|
315
475
|
}
|
|
316
476
|
}
|
|
317
477
|
}
|
|
@@ -320,7 +480,7 @@ class FileLock {
|
|
|
320
480
|
this.currentLock = null;
|
|
321
481
|
this.currentPidFile = null;
|
|
322
482
|
|
|
323
|
-
return
|
|
483
|
+
return success;
|
|
324
484
|
}
|
|
325
485
|
|
|
326
486
|
/**
|
|
@@ -358,7 +518,10 @@ class FileLock {
|
|
|
358
518
|
}
|
|
359
519
|
|
|
360
520
|
/**
|
|
361
|
-
* Wait for a lock to be released
|
|
521
|
+
* Wait for a lock to be released (SYNCHRONOUS - may block event loop)
|
|
522
|
+
*
|
|
523
|
+
* WARNING: This method uses a busy-wait loop that blocks the Node.js event loop.
|
|
524
|
+
* For non-blocking operation, use waitForLockAsync() instead.
|
|
362
525
|
*
|
|
363
526
|
* @param {string} filePath - Path to wait for
|
|
364
527
|
* @param {number} maxWait - Maximum milliseconds to wait (default: timeout option)
|
|
@@ -383,6 +546,78 @@ class FileLock {
|
|
|
383
546
|
return true;
|
|
384
547
|
}
|
|
385
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Acquire an exclusive lock asynchronously (yields to event loop)
|
|
551
|
+
*
|
|
552
|
+
* This is the non-blocking version of acquire(). It uses setTimeout for
|
|
553
|
+
* delays, allowing other async operations to run during wait periods.
|
|
554
|
+
*
|
|
555
|
+
* @param {string} filePath - Path to the file to lock
|
|
556
|
+
* @returns {Promise<boolean>} True if lock acquired, false on timeout
|
|
557
|
+
* @throws {FileSystemError} On unexpected filesystem errors
|
|
558
|
+
*/
|
|
559
|
+
async acquireAsync(filePath) {
|
|
560
|
+
const { lockFile, pidFile } = this._getLockPaths(filePath);
|
|
561
|
+
|
|
562
|
+
// Check for existing lock and handle stale locks
|
|
563
|
+
if (fs.existsSync(lockFile)) {
|
|
564
|
+
this._cleanupStaleLock(lockFile, pidFile);
|
|
565
|
+
// Lock is held by running process if not cleaned
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Try to acquire lock with retries
|
|
569
|
+
let retryCount = 0;
|
|
570
|
+
const startTime = Date.now();
|
|
571
|
+
|
|
572
|
+
while (retryCount < this.options.maxRetries) {
|
|
573
|
+
// Try to acquire the lock
|
|
574
|
+
if (this._tryAcquire(lockFile, pidFile)) {
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Check timeout
|
|
579
|
+
if (Date.now() - startTime >= this.options.timeout) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Wait before retry (ASYNC - yields to event loop)
|
|
584
|
+
retryCount++;
|
|
585
|
+
if (retryCount < this.options.maxRetries) {
|
|
586
|
+
await new Promise(resolve =>
|
|
587
|
+
setTimeout(resolve, this.options.retryInterval)
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Wait for a lock to be released asynchronously (yields to event loop)
|
|
597
|
+
*
|
|
598
|
+
* This is the non-blocking version of waitForLock(). It uses setTimeout
|
|
599
|
+
* for delays, allowing other async operations to run during wait periods.
|
|
600
|
+
*
|
|
601
|
+
* @param {string} filePath - Path to wait for
|
|
602
|
+
* @param {number} maxWait - Maximum milliseconds to wait (default: timeout option)
|
|
603
|
+
* @returns {Promise<boolean>} True if lock was released, false on timeout
|
|
604
|
+
*/
|
|
605
|
+
async waitForLockAsync(filePath, maxWait = null) {
|
|
606
|
+
const waitTime = maxWait || this.options.timeout;
|
|
607
|
+
const startTime = Date.now();
|
|
608
|
+
|
|
609
|
+
while (this.isLocked(filePath)) {
|
|
610
|
+
if (Date.now() - startTime >= waitTime) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Small async delay (yields to event loop)
|
|
615
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
386
621
|
/**
|
|
387
622
|
* Force cleanup of all locks in the lock directory
|
|
388
623
|
*
|