import * as path from 'path' ; import * as child_process from 'child_process' ; import { FileSystem } from './FileSystem' ; import { FileWriter } from './FileWriter' ; import { Async } from './Async' ; /**
 * http://man7.org/linux/man-pages/man5/proc.5.html
 * (22) starttime  %llu
 * The time the process started after system boot. In kernels before Linux 2.6, this value was
 * expressed in jiffies. Since Linux 2.6, the value is expressed in clock ticks (divide by
 * sysconf(_SC_CLK_TCK)).
 * The format for this field was %lu before Linux 2.6.
 */ const procStatStartTimePos = 22 ; param stat returns export function getProcessStartTimeFromProcStat ( stat ) { let values = stat . trimRight ( ) . split ( ' ' ) ; let i = values . length - 1 ; while ( i >= 0 && values [ i ] . charAt ( values [ i ] . length - 1 ) !== ')' ) { i -= 1 ; } if ( i < 1 ) { return undefined ; } const value2 = values . slice ( 1 , i + 1 ) . join ( ' ' ) ; values = [ values [ 0 ] , value2 ] . concat ( values . slice ( i + 1 ) ) ; if ( values . length < procStatStartTimePos ) { return undefined ; } const startTimeJiffies = values [ procStatStartTimePos - 1 ] ; return startTimeJiffies ; } /**
 * Helper function that is exported for unit tests only.
 * Returns undefined if the process doesn't exist with that pid.
 */ export function getProcessStartTime ( pid ) { const pidString = pid . toString ( ) ; if ( pid < 0 || pidString . indexOf ( 'e' ) >= 0 || pidString . indexOf ( 'E' ) >= 0 ) { throw new Error ( `"pid" is negative or too large` ) ; } let args ; if ( process . platform === 'darwin' ) { args = [ `-p ${ pidString }` , '-o lstart' ] ; } else if ( process . platform === 'linux' ) { args = [ '-p' , pidString , '-o' , 'lstart' ] ; } else { throw new Error ( `Unsupported system: ${ process . platform }` ) ; } const psResult = child_process . spawnSync ( 'ps' , args , { encoding : 'utf8' } ) ; const psStdout = psResult . stdout ; if ( psResult . status !== 0 && ! psStdout && process . platform === 'linux' ) { let stat ; try { stat = FileSystem . readFile ( `/proc/${ pidString }/stat` ) ; } catch ( error ) { if ( ( error as NodeJS . ErrnoException ) . code !== 'ENOENT' ) { throw error ; } return undefined ; } if ( stat !== undefined ) { const startTimeJiffies = getProcessStartTimeFromProcStat ( stat ) ; if ( startTimeJiffies === undefined ) { throw new Error ( `Could not retrieve the start time of process ${ pidString } from the OS because the ` + `contents of /proc/${ pidString }/stat have an unexpected format` ) ; } return startTimeJiffies ; } } if ( ! psStdout ) { throw new Error ( `Unexpected output from "ps" command` ) ; } const psSplit = psStdout . split ( '\n' ) ; if ( psSplit [ 1 ] === '' ) { return undefined ; } if ( psSplit [ 1 ] ) { const trimmed = psSplit [ 1 ] . trim ( ) ; if ( trimmed . length > 10 ) { return trimmed ; } } throw new Error ( `Unexpected output from the "ps" command` ) ; } remarks public export class LockFile { private static _getStartTime : ( pid : number ) => string | undefined = getProcessStartTime ; private _fileWriter : FileWriter | undefined ; private _filePath : string ; private _dirtyWhenAcquired : boolean ; private constructor ( fileWriter , filePath , dirtyWhenAcquired ) { this . _fileWriter = fileWriter ; this . _filePath = filePath ; this . _dirtyWhenAcquired = dirtyWhenAcquired ; } param resourceFolder param resourceName param pid public static getLockFilePath ( resourceFolder , resourceName , pid = process . pid ) { if ( ! resourceName . match ( /^[a-zA-Z0-9][a-zA-Z0-9-.]+[a-zA-Z0-9]$/ ) ) { throw new Error ( `The resource name "${ resourceName }" is invalid.` + ` It must be an alphanumberic string with only "-" or "." It must start with an alphanumeric character.` ) ; } if ( process . platform === 'win32' ) { return path . join ( path . resolve ( resourceFolder ) , `${ resourceName }.lock` ) ; } else if ( process . platform === 'linux' || process . platform === 'darwin' ) { return path . join ( path . resolve ( resourceFolder ) , `${ resourceName }#${ pid }.lock` ) ; } throw new Error ( `File locking not implemented for platform: "${ process . platform }"` ) ; } param resourceFolder param resourceName returns public static tryAcquire ( resourceFolder , resourceName ) { FileSystem . ensureFolder ( resourceFolder ) ; if ( process . platform === 'win32' ) { return LockFile . _tryAcquireWindows ( resourceFolder , resourceName ) ; } else if ( process . platform === 'linux' || process . platform === 'darwin' ) { return LockFile . _tryAcquireMacOrLinux ( resourceFolder , resourceName ) ; } throw new Error ( `File locking not implemented for platform: "${ process . platform }"` ) ; } remarks param resourceFolder param resourceName param maxWaitMs public static acquire ( resourceFolder , resourceName , maxWaitMs ? ) { const interval = 100 ; const startTime = Date . now ( ) ; const retryLoop = async ( ) => { const lock = LockFile . tryAcquire ( resourceFolder , resourceName ) ; if ( lock ) { return lock ; } if ( maxWaitMs && Date . now ( ) > startTime + maxWaitMs ) { throw new Error ( `Exceeded maximum wait time to acquire lock for resource "${ resourceName }"` ) ; } await Async . sleep ( interval ) ; return retryLoop ( ) ; } ; return retryLoop ( ) ; } /**
     * Attempts to acquire the lock on a Linux or OSX machine
     */ private static _tryAcquireMacOrLinux ( resourceFolder , resourceName ) { let dirtyWhenAcquired = false ; const pid = process . pid ; const startTime = LockFile . _getStartTime ( pid ) ; if ( ! startTime ) { throw new Error ( `Unable to calculate start time for current process.` ) ; } const pidLockFilePath = LockFile . getLockFilePath ( resourceFolder , resourceName ) ; let lockFileHandle ; let lockFile ; try { lockFileHandle = FileWriter . open ( pidLockFilePath ) ; lockFileHandle . write ( startTime ) ; const currentBirthTimeMs = FileSystem . getStatistics ( pidLockFilePath ) . birthtime . getTime ( ) ; let smallestBirthTimeMs = currentBirthTimeMs ; let smallestBirthTimePid = pid . toString ( ) ; const files = FileSystem . readFolderItemNames ( resourceFolder ) ; const lockFileRegExp = /^(.+)#([0-9]+)\.lock$/ ; let match ; let otherPid ; for ( const fileInFolder of files ) { if ( ( match = fileInFolder . match ( lockFileRegExp ) ) && match [ 1 ] === resourceName && ( otherPid = match [ 2 ] ) !== pid . toString ( ) ) { const fileInFolderPath = path . join ( resourceFolder , fileInFolder ) ; dirtyWhenAcquired = true ; const otherPidCurrentStartTime = LockFile . _getStartTime ( parseInt ( otherPid , 10 ) ) ; let otherPidOldStartTime ; let otherBirthtimeMs ; try { otherPidOldStartTime = FileSystem . readFile ( fileInFolderPath ) ; otherBirthtimeMs = FileSystem . getStatistics ( fileInFolderPath ) . birthtime . getTime ( ) ; } catch ( err ) { } if ( otherPidOldStartTime === '' && otherBirthtimeMs !== undefined ) { if ( otherBirthtimeMs > currentBirthTimeMs ) { continue ; } else if ( otherBirthtimeMs - currentBirthTimeMs < 0 && otherBirthtimeMs - currentBirthTimeMs > - 1000 ) { return undefined ; } } if ( ! otherPidCurrentStartTime || otherPidOldStartTime !== otherPidCurrentStartTime ) { FileSystem . deleteFile ( fileInFolderPath ) ; continue ; } if ( otherBirthtimeMs !== undefined && otherBirthtimeMs < smallestBirthTimeMs ) { smallestBirthTimeMs = otherBirthtimeMs ; smallestBirthTimePid = otherPid ; } } } if ( smallestBirthTimePid !== pid . toString ( ) ) { return undefined ; } lockFile = new LockFile ( lockFileHandle , pidLockFilePath , dirtyWhenAcquired ) ; lockFileHandle = undefined ; } finally { if ( lockFileHandle ) { lockFileHandle . close ( ) ; FileSystem . deleteFile ( pidLockFilePath ) ; } } return lockFile ; } /**
     * Attempts to acquire the lock using Windows
     * This algorithm is much simpler since we can rely on the operating system
     */ private static _tryAcquireWindows ( resourceFolder , resourceName ) { const lockFilePath = LockFile . getLockFilePath ( resourceFolder , resourceName ) ; let dirtyWhenAcquired = false ; let fileHandle ; let lockFile ; try { if ( FileSystem . exists ( lockFilePath ) ) { dirtyWhenAcquired = true ; FileSystem . deleteFile ( lockFilePath ) ; } try { fileHandle = FileWriter . open ( lockFilePath , { exclusive : true } ) ; } catch ( error ) { return undefined ; } lockFile = new LockFile ( fileHandle , lockFilePath , dirtyWhenAcquired ) ; fileHandle = undefined ; } finally { if ( fileHandle ) { fileHandle . close ( ) ; } } return lockFile ; } /**
     * Unlocks a file and removes it from disk.
     * This can only be called once.
     */ public release ( ) { if ( this . isReleased ) { throw new Error ( `The lock for file "${ path . basename ( this . _filePath ) }" has already been released.` ) ; } this . _fileWriter ! . close ( ) ; FileSystem . deleteFile ( this . _filePath ) ; this . _fileWriter = undefined ; } /**
     * Returns the initial state of the lock.
     * This can be used to detect if the previous process was terminated before releasing the resource.
     */ public get dirtyWhenAcquired ( ) : boolean { return this . _dirtyWhenAcquired ; } /**
     * Returns the absolute path to the lockfile
     */ public get filePath ( ) : string { return this . _filePath ; } /**
     * Returns true if this lock is currently being held.
     */ public get isReleased ( ) : boolean { return this . _fileWriter === undefined ; } }