From 8f62aa933562d37f344015cf4e43775fbf81716b Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Mon, 9 Jan 2023 17:13:58 -0800 Subject: [PATCH] chore: vendor proper-lockfile dependency (#19969) This patch vendors the https://github.com/moxystudio/node-proper-lockfile project we rely to manage Playwright Registry. The reason to vender is the following upstream issue that didn't get resolved in almost a month: https://github.com/moxystudio/node-proper-lockfile/issues/111 Follow-up will apply the fix for the issue to the vendored file. NOTE: this patch also inlines all code in a single file. References #19418 --- .../playwright-core/ThirdPartyNotices.txt | 29 +- .../bundles/utils/package-lock.json | 40 +- .../bundles/utils/package.json | 4 +- .../bundles/utils/src/third_party/lockfile.js | 396 ++++++++++++++++++ .../bundles/utils/src/utilsBundleImpl.ts | 2 +- 5 files changed, 412 insertions(+), 59 deletions(-) create mode 100644 packages/playwright-core/bundles/utils/src/third_party/lockfile.js diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 76cf98ba07322..90d4d0135f516 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -36,7 +36,6 @@ This project incorporates components from the projects listed below. The origina - pend@1.2.0 (https://github.com/andrewrk/node-pend) - pngjs@6.0.0 (https://github.com/lukeapage/pngjs) - progress@2.0.3 (https://github.com/visionmedia/node-progress) -- proper-lockfile@4.1.2 (https://github.com/moxystudio/node-proper-lockfile) - proxy-from-env@1.1.0 (https://github.com/Rob--W/proxy-from-env) - pump@3.0.0 (https://github.com/mafintosh/pump) - retry@0.12.0 (https://github.com/tim-kos/node-retry) @@ -1166,32 +1165,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF progress@2.0.3 AND INFORMATION -%% proper-lockfile@4.1.2 NOTICES AND INFORMATION BEGIN HERE -========================================= -The MIT License (MIT) - -Copyright (c) 2018 Made With MOXY Lda - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF proper-lockfile@4.1.2 AND INFORMATION - %% proxy-from-env@1.1.0 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License @@ -1641,6 +1614,6 @@ END OF yazl@2.5.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 46 +Total Packages: 45 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index dce6ac47e12a9..15b97625db7cb 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -12,15 +12,17 @@ "commander": "8.3.0", "debug": "^4.3.4", "glob": "^7.1.3", + "graceful-fs": "4.2.10", "https-proxy-agent": "5.0.0", "jpeg-js": "0.4.4", "mime": "^3.0.0", "minimatch": "^3.1.2", "pngjs": "6.0.0", "progress": "2.0.3", - "proper-lockfile": "4.1.2", "proxy-from-env": "1.1.0", + "retry": "0.12.0", "rimraf": "^3.0.2", + "signal-exit": "3.0.7", "socks-proxy-agent": "6.1.1", "stack-utils": "2.0.5", "ws": "8.4.2" @@ -119,9 +121,9 @@ } }, "node_modules/@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, "node_modules/@types/rimraf": { @@ -343,16 +345,6 @@ "node": ">=0.4.0" } }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -361,7 +353,7 @@ "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "engines": { "node": ">= 4" } @@ -538,9 +530,9 @@ } }, "@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, "@types/rimraf": { @@ -715,16 +707,6 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" }, - "proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "requires": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -733,7 +715,7 @@ "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" }, "rimraf": { "version": "3.0.2", diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index 98bb30e8afbe2..5934f2a4bb110 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -13,15 +13,17 @@ "commander": "8.3.0", "debug": "^4.3.4", "glob": "^7.1.3", + "graceful-fs": "4.2.10", "https-proxy-agent": "5.0.0", "jpeg-js": "0.4.4", "mime": "^3.0.0", "minimatch": "^3.1.2", "pngjs": "6.0.0", "progress": "2.0.3", - "proper-lockfile": "4.1.2", "proxy-from-env": "1.1.0", + "retry": "0.12.0", "rimraf": "^3.0.2", + "signal-exit": "3.0.7", "socks-proxy-agent": "6.1.1", "stack-utils": "2.0.5", "ws": "8.4.2" diff --git a/packages/playwright-core/bundles/utils/src/third_party/lockfile.js b/packages/playwright-core/bundles/utils/src/third_party/lockfile.js new file mode 100644 index 0000000000000..c7e2f196f42c4 --- /dev/null +++ b/packages/playwright-core/bundles/utils/src/third_party/lockfile.js @@ -0,0 +1,396 @@ +/** + * + * The MIT License (MIT) + * + * Copyright (c) 2018 Made With MOXY Lda + * Modifications copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +'use strict'; + +const path = require('path'); +const fs = require('graceful-fs'); +const retry = require('retry'); +const onExit = require('signal-exit'); + +const locks = {}; +const cacheSymbol = Symbol(); + +function probe(file, fs, callback) { + const cachedPrecision = fs[cacheSymbol]; + + if (cachedPrecision) { + return fs.stat(file, (err, stat) => { + /* istanbul ignore if */ + if (err) { + return callback(err); + } + + callback(null, stat.mtime, cachedPrecision); + }); + } + + // Set mtime by ceiling Date.now() to seconds + 5ms so that it's "not on the second" + const mtime = new Date((Math.ceil(Date.now() / 1000) * 1000) + 5); + + fs.utimes(file, mtime, mtime, (err) => { + /* istanbul ignore if */ + if (err) { + return callback(err); + } + + fs.stat(file, (err, stat) => { + /* istanbul ignore if */ + if (err) { + return callback(err); + } + + const precision = stat.mtime.getTime() % 1000 === 0 ? 's' : 'ms'; + + // Cache the precision in a non-enumerable way + Object.defineProperty(fs, cacheSymbol, { value: precision }); + + callback(null, stat.mtime, precision); + }); + }); +} + +function getMtime(precision) { + let now = Date.now(); + + if (precision === 's') { + now = Math.ceil(now / 1000) * 1000; + } + + return new Date(now); +} + +function getLockFile(file, options) { + return options.lockfilePath || `${file}.lock`; +} + +function resolveCanonicalPath(file, options, callback) { + if (!options.realpath) { + return callback(null, path.resolve(file)); + } + + // Use realpath to resolve symlinks + // It also resolves relative paths + options.fs.realpath(file, callback); +} + +function acquireLock(file, options, callback) { + const lockfilePath = getLockFile(file, options); + + // Use mkdir to create the lockfile (atomic operation) + options.fs.mkdir(lockfilePath, (err) => { + if (!err) { + // At this point, we acquired the lock! + // Probe the mtime precision + return probe(lockfilePath, options.fs, (err, mtime, mtimePrecision) => { + // If it failed, try to remove the lock.. + /* istanbul ignore if */ + if (err) { + options.fs.rmdir(lockfilePath, () => {}); + + return callback(err); + } + + callback(null, mtime, mtimePrecision); + }); + } + + // If error is not EEXIST then some other error occurred while locking + if (err.code !== 'EEXIST') { + return callback(err); + } + + // Otherwise, check if lock is stale by analyzing the file mtime + if (options.stale <= 0) { + return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file })); + } + + options.fs.stat(lockfilePath, (err, stat) => { + if (err) { + // Retry if the lockfile has been removed (meanwhile) + // Skip stale check to avoid recursiveness + if (err.code === 'ENOENT') { + return acquireLock(file, { ...options, stale: 0 }, callback); + } + + return callback(err); + } + + if (!isLockStale(stat, options)) { + return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file })); + } + + // If it's stale, remove it and try again! + // Skip stale check to avoid recursiveness + removeLock(file, options, (err) => { + if (err) { + return callback(err); + } + + acquireLock(file, { ...options, stale: 0 }, callback); + }); + }); + }); +} + +function isLockStale(stat, options) { + return stat.mtime.getTime() < Date.now() - options.stale; +} + +function removeLock(file, options, callback) { + // Remove lockfile, ignoring ENOENT errors + options.fs.rmdir(getLockFile(file, options), (err) => { + if (err && err.code !== 'ENOENT') { + return callback(err); + } + + callback(); + }); +} + +function updateLock(file, options) { + const lock = locks[file]; + + // Just for safety, should never happen + /* istanbul ignore if */ + if (lock.updateTimeout) { + return; + } + + lock.updateDelay = lock.updateDelay || options.update; + lock.updateTimeout = setTimeout(() => { + lock.updateTimeout = null; + + // Stat the file to check if mtime is still ours + // If it is, we can still recover from a system sleep or a busy event loop + options.fs.stat(lock.lockfilePath, (err, stat) => { + const isOverThreshold = lock.lastUpdate + options.stale < Date.now(); + + // If it failed to update the lockfile, keep trying unless + // the lockfile was deleted or we are over the threshold + if (err) { + if (err.code === 'ENOENT' || isOverThreshold) { + return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' })); + } + + lock.updateDelay = 1000; + + return updateLock(file, options); + } + + const isMtimeOurs = lock.mtime.getTime() === stat.mtime.getTime(); + + if (!isMtimeOurs) { + return setLockAsCompromised( + file, + lock, + Object.assign( + new Error('Unable to update lock within the stale threshold'), + { code: 'ECOMPROMISED' } + )); + } + + const mtime = getMtime(lock.mtimePrecision); + + options.fs.utimes(lock.lockfilePath, mtime, mtime, (err) => { + const isOverThreshold = lock.lastUpdate + options.stale < Date.now(); + + // Ignore if the lock was released + if (lock.released) { + return; + } + + // If it failed to update the lockfile, keep trying unless + // the lockfile was deleted or we are over the threshold + if (err) { + if (err.code === 'ENOENT' || isOverThreshold) { + return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' })); + } + + lock.updateDelay = 1000; + + return updateLock(file, options); + } + + // All ok, keep updating.. + lock.mtime = mtime; + lock.lastUpdate = Date.now(); + lock.updateDelay = null; + updateLock(file, options); + }); + }); + }, lock.updateDelay); + + // Unref the timer so that the nodejs process can exit freely + // This is safe because all acquired locks will be automatically released + // on process exit + + // We first check that `lock.updateTimeout.unref` exists because some users + // may be using this module outside of NodeJS (e.g., in an electron app), + // and in those cases `setTimeout` return an integer. + /* istanbul ignore else */ + if (lock.updateTimeout.unref) { + lock.updateTimeout.unref(); + } +} + +function setLockAsCompromised(file, lock, err) { + // Signal the lock has been released + lock.released = true; + + // Cancel lock mtime update + // Just for safety, at this point updateTimeout should be null + /* istanbul ignore if */ + if (lock.updateTimeout) { + clearTimeout(lock.updateTimeout); + } + + if (locks[file] === lock) { + delete locks[file]; + } + + lock.options.onCompromised(err); +} + +// ---------------------------------------------------------- + +function lock(file, options, callback) { + /* istanbul ignore next */ + options = { + stale: 10000, + update: null, + realpath: true, + retries: 0, + fs, + onCompromised: (err) => { throw err; }, + ...options, + }; + + options.retries = options.retries || 0; + options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries; + options.stale = Math.max(options.stale || 0, 2000); + options.update = options.update == null ? options.stale / 2 : options.update || 0; + options.update = Math.max(Math.min(options.update, options.stale / 2), 1000); + + // Resolve to a canonical file path + resolveCanonicalPath(file, options, (err, file) => { + if (err) { + return callback(err); + } + + // Attempt to acquire the lock + const operation = retry.operation(options.retries); + + operation.attempt(() => { + acquireLock(file, options, (err, mtime, mtimePrecision) => { + if (operation.retry(err)) { + return; + } + + if (err) { + return callback(operation.mainError()); + } + + // We now own the lock + const lock = locks[file] = { + lockfilePath: getLockFile(file, options), + mtime, + mtimePrecision, + options, + lastUpdate: Date.now(), + }; + + // We must keep the lock fresh to avoid staleness + updateLock(file, options); + + callback(null, (releasedCallback) => { + if (lock.released) { + return releasedCallback && + releasedCallback(Object.assign(new Error('Lock is already released'), { code: 'ERELEASED' })); + } + + // Not necessary to use realpath twice when unlocking + unlock(file, { ...options, realpath: false }, releasedCallback); + }); + }); + }); + }); +} + +function unlock(file, options, callback) { + options = { + fs, + realpath: true, + ...options, + }; + + // Resolve to a canonical file path + resolveCanonicalPath(file, options, (err, file) => { + if (err) { + return callback(err); + } + + // Skip if the lock is not acquired + const lock = locks[file]; + + if (!lock) { + return callback(Object.assign(new Error('Lock is not acquired/owned by you'), { code: 'ENOTACQUIRED' })); + } + + lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update + lock.released = true; // Signal the lock has been released + delete locks[file]; // Delete from locks + + removeLock(file, options, callback); + }); +} + +function toPromise(method) { + return (...args) => new Promise((resolve, reject) => { + args.push((err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + method(...args); + }); +} + +// Remove acquired locks on exit +/* istanbul ignore next */ +onExit(() => { + for (const file in locks) { + const options = locks[file].options; + + try { options.fs.rmdirSync(getLockFile(file, options)); } catch (e) { /* Empty */ } + } +}); + +module.exports.lock = async (file, options) => { + const release = await toPromise(lock)(file, options); + return toPromise(release); +} diff --git a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts index 1e8123888a443..ee081796f69bb 100644 --- a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts @@ -27,7 +27,7 @@ export { HttpsProxyAgent } from 'https-proxy-agent'; import jpegLibrary from 'jpeg-js'; export const jpegjs = jpegLibrary; -import lockfileLibrary from 'proper-lockfile'; +const lockfileLibrary = require('./third_party/lockfile'); export const lockfile = lockfileLibrary; import mimeLibrary from 'mime';