@@ -296,10 +296,13 @@ export class LockFile {
296296 resourceName : string ,
297297 pidLockFilePath : string
298298 ) : LockFile | undefined {
299- let dirtyWhenAcquired : boolean = false ;
300-
301- // get the current process' pid
299+ // get the current process identifier (PID)
302300 const pid : number = process . pid ;
301+
302+ // Suppose that a process terminates unexpectedly without deleting its PID-based lockfile,
303+ // then we check to see if the process is still alive. The OS may have given the same PID
304+ // to a new process, how to detect that? We will rely on getProcessStartTime() which
305+ // is stored in the file itself for comparison.
303306 const startTime : string | undefined = LockFile . _getStartTime ( pid ) ;
304307
305308 if ( ! startTime ) {
@@ -327,6 +330,11 @@ export class LockFile {
327330 // look for anything ending with # then numbers and ".lock"
328331 const lockFileRegExp : RegExp = / ^ ( .+ ) # ( [ 0 - 9 ] + ) \. l o c k $ / ;
329332
333+ // If we are the process to acquire the lock, it becomes our responsibility to clean up these
334+ // stale files. If there is at least 1 stale file, then the resource is assumed to be "dirty"
335+ // (for example, the previous process was interrupted before releasing or while acquiring).
336+ const staleFilesToDelete : string [ ] = [ ] ;
337+
330338 let match : RegExpMatchArray | null ;
331339 let otherPid : string ;
332340 for ( const fileInFolder of files ) {
@@ -335,14 +343,16 @@ export class LockFile {
335343 match [ 1 ] === resourceName &&
336344 ( otherPid = match [ 2 ] ) !== pid . toString ( )
337345 ) {
338- // we found at least one lockfile hanging around that isn't ours
346+ // We found at least one lockfile hanging around that isn't ours
339347 const fileInFolderPath : string = `${ resourceFolder } /${ fileInFolder } ` ;
340- dirtyWhenAcquired = true ;
341348
342349 // console.log(`FOUND OTHER LOCKFILE: ${otherPid}`);
343350
351+ // Actual start time of the other PID
344352 const otherPidCurrentStartTime : string | undefined = LockFile . _getStartTime ( parseInt ( otherPid , 10 ) ) ;
345353
354+ // The start time from the file, which we will compare with otherPidCurrentStartTime
355+ // to determine whether the PID got reused by a new process.
346356 let otherPidOldStartTime : string | undefined ;
347357 let otherBirthtimeMs : number | undefined ;
348358 try {
@@ -351,47 +361,62 @@ export class LockFile {
351361 otherBirthtimeMs = FileSystem . getStatistics ( fileInFolderPath ) . birthtime . getTime ( ) ;
352362 } catch ( error ) {
353363 if ( FileSystem . isNotExistError ( error ) ) {
354- // the file is already deleted by other process, skip it
364+ // ==> Properly closed lockfile, safe to ignore:
365+ // The other process deleted the file, which we assume means it completed successfully,
366+ // so the state is not dirty. This is equivalent to if readFolderItemNames() never saw
367+ // the file in the firstplace.
355368 continue ;
356369 }
357370 }
358371
359- // if the otherPidOldStartTime is invalid, then we should look at the timestamp,
360- // if this file was created after us, ignore it
361- // if it was created within 1 second before us, then it could be good, so we
362- // will conservatively fail
363- // otherwise it is an old lock file and will be deleted
364- if ( otherPidOldStartTime === '' && otherBirthtimeMs !== undefined ) {
372+ // What the other process's file exists, but it is an empty file?
373+ // Either they were terminated while acquiring, or else they haven't finished writing it yet.
374+ if ( otherBirthtimeMs !== undefined && otherPidOldStartTime === '' ) {
365375 if ( otherBirthtimeMs > currentBirthTimeMs ) {
366- // ignore this file, he will be unable to get the lock since this process
367- // will hold it
376+ // ==> Safe to ignore
377+ // If the other process was terminated, it happened before they finished acquiring.
378+ // If the other process is alive, their file is newer, so we will acquire instead of them.
379+
368380 // console.log(`Ignoring lock for pid ${otherPid} because its lockfile is newer than ours.`);
369381 continue ;
370382 } else if (
371- otherBirthtimeMs - currentBirthTimeMs < 0 && // it was created before us AND
383+ otherBirthtimeMs - currentBirthTimeMs < 0 &&
372384 otherBirthtimeMs - currentBirthTimeMs > - 1000
373385 ) {
374- // it was created less than a second before
375-
376- // conservatively be unable to keep the lock
377- return undefined ;
386+ // ==> Race condition
387+ // The other process created their file first, so they will probably acquire the lock
388+ // after they finish writing the contents. But what if their process is actually dead
389+ // and replaced by a new process with the same PID? Normally the otherPidOldStartTime
390+ // gives the answer, but in this edge case we are missing that information.
391+ // So we conservatively assume that it should not take them more than 1000ms to
392+ // open a file, write a PID, and close the file.
393+ return undefined ; // fail to acquire and retry later
378394 }
379395 }
380396
381397 // console.log(`Other pid ${otherPid} lockfile has start time: "${otherPidOldStartTime}"`);
382398 // console.log(`Other pid ${otherPid} actually has start time: "${otherPidCurrentStartTime}"`);
383399
384- // this means the process is no longer executing, delete the file
400+ // Time to compare
385401 if ( ! otherPidCurrentStartTime || otherPidOldStartTime !== otherPidCurrentStartTime ) {
402+ // ==> Stale lockfile
403+ // This file doesn't prevent us from acquiring the lock, but it does indicate that
404+ // the resource was left in a dirty state. (If we delete the file right now, that
405+ // information would be lost, so we clean up later when we acquire successfully.)
406+
386407 // console.log(`Other pid ${otherPid} is no longer executing!`);
387- FileSystem . deleteFile ( fileInFolderPath ) ;
408+ staleFilesToDelete . push ( fileInFolderPath ) ;
388409 continue ;
389410 }
390411
391412 // console.log(`Pid ${otherPid} lockfile has birth time: ${otherBirthtimeMs}`);
392413 // console.log(`Pid ${pid} lockfile has birth time: ${currentBirthTimeMs}`);
393- // this is a lockfile pointing at something valid
414+
394415 if ( otherBirthtimeMs !== undefined ) {
416+ // ==> We found a valid file belonging to another process.
417+ // With multiple parties trying to acquire, the winner is the smallestBirthTime,
418+ // so we need to sort.
419+
395420 // the other lock file was created before the current earliest lock file
396421 // or the other lock file was created at the same exact time, but has earlier pid
397422
@@ -418,9 +443,15 @@ export class LockFile {
418443 return undefined ;
419444 }
420445
421- // we have the lock!
446+ let dirtyWhenAcquired : boolean = false ;
447+ for ( const staleFileToDelete of staleFilesToDelete ) {
448+ FileSystem . deleteFile ( staleFileToDelete , { throwIfNotExists : false } ) ;
449+ dirtyWhenAcquired = true ;
450+ }
451+
452+ // We have the lock!
422453 lockFile = new LockFile ( lockFileHandle , pidLockFilePath , dirtyWhenAcquired ) ;
423- lockFileHandle = undefined ; // we have handed the descriptor off to the instance
454+ lockFileHandle = undefined ; // The LockFile object has taken ownership of our handle
424455 } finally {
425456 if ( lockFileHandle ) {
426457 // ensure our lock is closed
0 commit comments