2020-01-10 09:54:58 +00:00
/ * *
* @license
* Copyright Google Inc . All Rights Reserved .
*
* Use of this source code is governed by an MIT - style license that can be
* found in the LICENSE file at https : //angular.io/license
* /
import * as process from 'process' ;
2020-03-04 12:35:52 +00:00
import { AbsoluteFsPath , CachedFileSystem , FileSystem } from '../../../src/ngtsc/file_system' ;
2020-02-03 20:25:15 +00:00
import { Logger } from '../logging/logger' ;
2020-03-04 12:35:52 +00:00
let _lockFilePath : AbsoluteFsPath ;
export function getLockFilePath ( fs : FileSystem ) {
if ( ! _lockFilePath ) {
_lockFilePath =
fs . resolve ( require . resolve ( '@angular/compiler-cli/ngcc' ) , '../__ngcc_lock_file__' ) ;
}
return _lockFilePath ;
}
export interface LockFile {
path : AbsoluteFsPath ;
/ * *
* Write a lock file to disk containing the PID of the current process .
* /
write ( ) : void ;
2020-01-10 09:54:58 +00:00
2020-03-04 12:35:52 +00:00
/ * *
* Read the PID , of the process holding the lock , from the lockFile .
*
* It is feasible that the lockFile was removed between the call to ` write() ` that effectively
* checks for existence and this attempt to read the file . If so then this method should just
* gracefully return ` "{unknown}" ` .
* /
read ( ) : string ;
/ * *
* Remove the lock file from disk , whether or not it exists .
* /
remove ( ) : void ;
}
export class LockFileWithSignalHandlers implements LockFile {
2020-02-03 20:25:15 +00:00
constructor ( protected fs : FileSystem ) { }
2020-01-10 09:54:58 +00:00
2020-03-04 12:35:52 +00:00
path = getLockFilePath ( this . fs ) ;
write ( ) : void {
2020-01-10 09:54:58 +00:00
try {
2020-02-03 20:25:15 +00:00
this . addSignalHandlers ( ) ;
2020-03-04 12:35:52 +00:00
// To avoid race conditions, we check for existence of the lockFile by actually trying to
// create it exclusively.
return this . fs . writeFile ( this . path , process . pid . toString ( ) , /* exclusive */ true ) ;
2020-02-03 20:25:15 +00:00
} catch ( e ) {
this . removeSignalHandlers ( ) ;
throw e ;
2020-01-10 09:54:58 +00:00
}
}
2020-03-04 12:35:52 +00:00
read ( ) : string {
2020-01-10 09:54:58 +00:00
try {
2020-02-03 20:25:15 +00:00
if ( this . fs instanceof CachedFileSystem ) {
// This file is "volatile", it might be changed by an external process,
// so we cannot rely upon the cached value when reading it.
2020-03-04 12:35:52 +00:00
this . fs . invalidateCaches ( this . path ) ;
2020-01-10 09:54:58 +00:00
}
2020-03-04 12:35:52 +00:00
return this . fs . readFile ( this . path ) ;
2020-02-03 20:25:15 +00:00
} catch {
return '{unknown}' ;
2020-01-10 09:54:58 +00:00
}
}
2020-03-04 12:35:52 +00:00
remove() {
2020-01-10 09:54:58 +00:00
this . removeSignalHandlers ( ) ;
2020-03-04 12:35:52 +00:00
if ( this . fs . exists ( this . path ) ) {
this . fs . removeFile ( this . path ) ;
2020-01-10 09:54:58 +00:00
}
}
2020-02-03 20:25:15 +00:00
/ * *
* Capture CTRL - C and terminal closing events .
2020-03-04 12:35:52 +00:00
* When these occur we remove the lockFile and exit .
2020-02-03 20:25:15 +00:00
* /
2020-01-10 09:54:58 +00:00
protected addSignalHandlers() {
2020-02-03 20:25:15 +00:00
process . addListener ( 'SIGINT' , this . signalHandler ) ;
process . addListener ( 'SIGHUP' , this . signalHandler ) ;
2020-01-10 09:54:58 +00:00
}
2020-02-03 20:25:15 +00:00
/ * *
* Clear the event handlers to prevent leakage .
* /
2020-01-10 09:54:58 +00:00
protected removeSignalHandlers() {
process . removeListener ( 'SIGINT' , this . signalHandler ) ;
process . removeListener ( 'SIGHUP' , this . signalHandler ) ;
}
/ * *
2020-02-03 20:25:15 +00:00
* This handler needs to be defined as a property rather than a method
2020-01-10 09:54:58 +00:00
* so that it can be passed around as a bound function .
* /
protected signalHandler =
( ) = > {
this . remove ( ) ;
this . exit ( 1 ) ;
}
/ * *
* This function wraps ` process.exit() ` which makes it easier to manage in unit tests ,
* since it is not possible to mock out ` process.exit() ` when it is called from signal handlers .
* /
protected exit ( code : number ) : void {
process . exit ( code ) ;
}
}
2020-02-03 20:25:15 +00:00
/ * *
2020-03-04 12:35:52 +00:00
* SyncLocker is used to prevent more than one instance of ngcc executing at the same time ,
2020-02-03 20:25:15 +00:00
* when being called in a synchronous context .
*
* * When ngcc starts executing , it creates a file in the ` compiler-cli/ngcc ` folder .
* * If it finds one is already there then it fails with a suitable error message .
* * When ngcc completes executing , it removes the file so that future ngcc executions can start .
* /
2020-03-04 12:35:52 +00:00
export class SyncLocker {
constructor ( private lockFile : LockFile ) { }
2020-02-03 20:25:15 +00:00
/ * *
* Run the given function guarded by the lock file .
*
* @param fn the function to run .
* @returns the value returned from the ` fn ` call .
* /
lock < T > ( fn : ( ) = > T ) : T {
this . create ( ) ;
try {
return fn ( ) ;
} finally {
2020-03-04 12:35:52 +00:00
this . lockFile . remove ( ) ;
2020-02-03 20:25:15 +00:00
}
}
/ * *
* Write a lock file to disk , or error if there is already one there .
* /
protected create ( ) : void {
try {
2020-03-04 12:35:52 +00:00
this . lockFile . write ( ) ;
2020-02-03 20:25:15 +00:00
} catch ( e ) {
if ( e . code !== 'EEXIST' ) {
throw e ;
}
this . handleExistingLockFile ( ) ;
}
}
/ * *
2020-03-04 12:35:52 +00:00
* The lockFile already exists so raise a helpful error .
2020-02-03 20:25:15 +00:00
* /
protected handleExistingLockFile ( ) : void {
2020-03-04 12:35:52 +00:00
const pid = this . lockFile . read ( ) ;
2020-02-03 20:25:15 +00:00
throw new Error (
` ngcc is already running at process with id ${ pid } . \ n ` +
` If you are running multiple builds in parallel then you should pre-process your node_modules via the command line ngcc tool before starting the builds; \ n ` +
` See https://v9.angular.io/guide/ivy#speeding-up-ngcc-compilation. \ n ` +
2020-03-04 12:35:52 +00:00
` (If you are sure no ngcc process is running then you should delete the lockFile at ${ this . lockFile . path } .) ` ) ;
2020-02-03 20:25:15 +00:00
}
}
/ * *
2020-03-04 12:35:52 +00:00
* AsyncLocker is used to prevent more than one instance of ngcc executing at the same time ,
2020-02-03 20:25:15 +00:00
* when being called in an asynchronous context .
*
* * When ngcc starts executing , it creates a file in the ` compiler-cli/ngcc ` folder .
* * If it finds one is already there then it pauses and waits for the file to be removed by the
* other process . If the file is not removed within a set timeout period given by
* ` retryDelay*retryAttempts ` an error is thrown with a suitable error message .
* * If the process locking the file changes , then we restart the timeout .
* * When ngcc completes executing , it removes the file so that future ngcc executions can start .
* /
2020-03-04 12:35:52 +00:00
export class AsyncLocker {
2020-02-03 20:25:15 +00:00
constructor (
2020-03-04 12:35:52 +00:00
private lockFile : LockFile , protected logger : Logger , private retryDelay : number ,
private retryAttempts : number ) { }
2020-02-03 20:25:15 +00:00
/ * *
* Run a function guarded by the lock file .
*
* @param fn The function to run .
* /
async lock < T > ( fn : ( ) = > Promise < T > ) : Promise < T > {
await this . create ( ) ;
2020-03-04 12:35:52 +00:00
return fn ( ) . finally ( ( ) = > this . lockFile . remove ( ) ) ;
2020-02-03 20:25:15 +00:00
}
protected async create() {
let pid : string = '' ;
for ( let attempts = 0 ; attempts < this . retryAttempts ; attempts ++ ) {
try {
2020-03-04 12:35:52 +00:00
return this . lockFile . write ( ) ;
2020-02-03 20:25:15 +00:00
} catch ( e ) {
if ( e . code !== 'EEXIST' ) {
throw e ;
}
2020-03-04 12:35:52 +00:00
const newPid = this . lockFile . read ( ) ;
2020-02-03 20:25:15 +00:00
if ( newPid !== pid ) {
// The process locking the file has changed, so restart the timeout
attempts = 0 ;
pid = newPid ;
}
if ( attempts === 0 ) {
this . logger . info (
` Another process, with id ${ pid } , is currently running ngcc. \ n ` +
` Waiting up to ${ this . retryDelay * this . retryAttempts / 1000 } s for it to finish. ` ) ;
}
// The file is still locked by another process so wait for a bit and retry
await new Promise ( resolve = > setTimeout ( resolve , this . retryDelay ) ) ;
}
}
// If we fall out of the loop then we ran out of rety attempts
throw new Error (
` Timed out waiting ${ this . retryAttempts * this . retryDelay / 1000 } s for another ngcc process, with id ${ pid } , to complete. \ n ` +
2020-03-04 12:35:52 +00:00
` (If you are sure no ngcc process is running then you should delete the lockFile at ${ this . lockFile . path } .) ` ) ;
2020-02-03 20:25:15 +00:00
}
}