
The `Logger` interface and its related classes are general purpose and could be used by other tooling. Moving it into ngtsc is a more suitable place from which to share it - similar to the FileSystem stuff. PR Close #37114
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC 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 {Logger} from '../../../src/ngtsc/logging';
|
|
import {NGCC_TIMED_OUT_EXIT_CODE} from '../constants';
|
|
|
|
import {LockFile} from './lock_file';
|
|
|
|
class TimeoutError extends Error {
|
|
code = NGCC_TIMED_OUT_EXIT_CODE;
|
|
}
|
|
|
|
/**
|
|
* AsyncLocker is used to prevent more than one instance of ngcc executing at the same time,
|
|
* 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.
|
|
*/
|
|
export class AsyncLocker {
|
|
constructor(
|
|
private lockFile: LockFile, protected logger: Logger, private retryDelay: number,
|
|
private retryAttempts: number) {}
|
|
|
|
/**
|
|
* 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();
|
|
return fn().finally(() => this.lockFile.remove());
|
|
}
|
|
|
|
protected async create() {
|
|
let pid: string = '';
|
|
for (let attempts = 0; attempts < this.retryAttempts; attempts++) {
|
|
try {
|
|
return this.lockFile.write();
|
|
} catch (e) {
|
|
if (e.code !== 'EEXIST') {
|
|
throw e;
|
|
}
|
|
const newPid = this.lockFile.read();
|
|
if (newPid !== pid) {
|
|
// The process locking the file has changed, so restart the timeout
|
|
attempts = 0;
|
|
pid = newPid;
|
|
}
|
|
if (attempts === 0) {
|
|
// Check to see if the process identified by the PID is still running. Because the
|
|
// process *should* clean up after itself, we only check for a stale lock file when the
|
|
// PID changes and only once. This may mean you have to wait if the process is killed
|
|
// after the first check and isn't given the chance to clean up after itself.
|
|
if (!this.isProcessRunning(pid)) {
|
|
// try to re-lock one last time in case there was a race condition checking the process.
|
|
try {
|
|
return this.lockFile.write();
|
|
} catch (e2) {
|
|
if (e2.code !== 'EEXIST') {
|
|
throw e2;
|
|
}
|
|
}
|
|
|
|
// finally check that the lock was held by the same process this whole time.
|
|
const finalPid = this.lockFile.read();
|
|
if (finalPid === pid) {
|
|
throw new TimeoutError(this.lockFileMessage(
|
|
`Lock found, but no process with PID ${pid} seems to be running.`));
|
|
} else {
|
|
// attempts is still 0, but adjust the PID so the message below is correct.
|
|
pid = finalPid;
|
|
}
|
|
}
|
|
|
|
this.logger.info(this.lockFileMessage(
|
|
`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 TimeoutError(this.lockFileMessage(`Timed out waiting ${
|
|
this.retryAttempts * this.retryDelay /
|
|
1000}s for another ngcc process, with id ${pid}, to complete.`));
|
|
}
|
|
|
|
protected isProcessRunning(pid: string): boolean {
|
|
// let the normal logic run if this is not called with a valid PID
|
|
if (isNaN(+pid)) {
|
|
this.logger.debug(`Cannot check if invalid PID "${pid}" is running, a number is expected.`);
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
process.kill(+pid, 0);
|
|
return true;
|
|
} catch (e) {
|
|
// If the process doesn't exist ESRCH will be thrown, if the error is not that, throw it.
|
|
if (e.code !== 'ESRCH') {
|
|
throw e;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private lockFileMessage(message: string): string {
|
|
return message +
|
|
`\n(If you are sure no ngcc process is running then you should delete the lock-file at ${
|
|
this.lockFile.path}.)`;
|
|
}
|
|
}
|