diff --git a/package-lock.json b/package-lock.json index dbcd99b3..29484d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", - "proper-lockfile": "^4.1.2", "semver": "^7.6.3" }, "devDependencies": { @@ -19,7 +18,6 @@ "@babel/preset-typescript": "^7.25.9", "@types/adm-zip": "^0.5.5", "@types/node": "^22.7.9", - "@types/proper-lockfile": "^4.1.4", "@types/semver": "^7.5.8", "babel-jest": "^29.7.0", "jest": "^29.7.0", @@ -2410,23 +2408,6 @@ "undici-types": "~6.19.8" } }, - "node_modules/@types/proper-lockfile": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz", - "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", - "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -3353,6 +3334,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -4722,17 +4704,6 @@ "node": ">= 6" } }, - "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==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -4893,15 +4864,6 @@ "node": ">=10" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4954,6 +4916,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/sisteransi": { diff --git a/package.json b/package.json index 30fb5c61..d7000dc3 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@babel/preset-typescript": "^7.25.9", "@types/adm-zip": "^0.5.5", "@types/node": "^22.7.9", - "@types/proper-lockfile": "^4.1.4", "@types/semver": "^7.5.8", "babel-jest": "^29.7.0", "jest": "^29.7.0", @@ -41,7 +40,6 @@ }, "dependencies": { "adm-zip": "^0.5.16", - "proper-lockfile": "^4.1.2", "semver": "^7.6.3" }, "repository": { diff --git a/src/libraries/Downloader.ts b/src/libraries/Downloader.ts index c32dc0c9..3af32998 100644 --- a/src/libraries/Downloader.ts +++ b/src/libraries/Downloader.ts @@ -6,9 +6,8 @@ import AdmZip from 'adm-zip' import { normalize as normalizePath } from 'path'; import { randomUUID } from 'crypto'; import { execFile } from 'child_process'; -import { lockSync } from 'proper-lockfile'; import { BinaryInfo, InternalServerOptions } from '../../types'; -import { waitForLock } from './FileLock'; +import { lockFile, waitForLock } from './FileLock'; function getZipData(entry: AdmZip.IZipEntry): Promise { return new Promise((resolve, reject) => { @@ -201,14 +200,14 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp return resolve(binaryPath) } - let releaseFunction: () => void; + let releaseFunction: () => Promise; while (true) { try { - releaseFunction = lockSync(extractedPath, {realpath: false}) + releaseFunction = await lockFile(extractedPath) break } catch (e) { - if (e.code === 'ELOCKED') { + if (e === 'LOCKED') { logger.log('Waiting for lock for MySQL version', version) await waitForLock(extractedPath, options) logger.log('Lock is gone for version', version) @@ -249,7 +248,7 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp if (downloadTries >= options.downloadRetries) { //Only reject if we have met the downloadRetries limit try { - releaseFunction() + await releaseFunction() } catch (e) { logger.error('An error occurred while releasing lock after downloadRetries exhaustion. The error was:', e) } diff --git a/src/libraries/Executor.ts b/src/libraries/Executor.ts index a1987e99..281b33ee 100644 --- a/src/libraries/Executor.ts +++ b/src/libraries/Executor.ts @@ -8,8 +8,7 @@ import { GenerateRandomPort } from "./Port"; import DBDestroySignal from "./AbortSignal"; import { ExecuteFileReturn, InstalledMySQLVersion, InternalServerOptions, MySQLDB } from "../../types"; import {normalize as normalizePath, resolve as resolvePath} from 'path' -import { lockSync } from 'proper-lockfile'; -import { waitForLock } from "./FileLock"; +import { lockFile, waitForLock } from "./FileLock"; class Executor { logger: Logger; @@ -312,14 +311,14 @@ class Executor { const copyPath = resolvePath(`${binaryFilepath}/../../lib/private/libaio.so.1`) - let lockRelease: () => void; + let lockRelease: () => Promise; while(true) { try { - lockRelease = lockSync(copyPath, {realpath: false}) + lockRelease = await lockFile(copyPath) break } catch (e) { - if (e.code === 'ELOCKED') { + if (e === 'LOCKED') { this.logger.log('Waiting for lock for libaio copy') await waitForLock(copyPath, options) this.logger.log('Lock is gone for libaio copy') @@ -359,7 +358,7 @@ class Executor { } finally { try { - lockRelease() + await lockRelease() } catch (e) { this.logger.error('Error unlocking libaio file:', e) } diff --git a/src/libraries/FileLock.ts b/src/libraries/FileLock.ts index 5f91e638..ef64285a 100644 --- a/src/libraries/FileLock.ts +++ b/src/libraries/FileLock.ts @@ -1,22 +1,73 @@ -import { checkSync } from "proper-lockfile"; +import fsPromises from 'fs/promises'; import { InternalServerOptions } from "../../types"; -export function waitForLock(path: string, options: InternalServerOptions): Promise { - return new Promise(async (resolve, reject) => { - let retries = 0; - while (retries <= options.lockRetries) { - retries++ +const mtimeUpdateIntervalTime = 2_000 +const mtimeLimit = 10_000 + +export async function waitForLock(path: string, options: InternalServerOptions): Promise { + const lockPath = `${path}.lock` + let retries = 0; + + do { + retries++; + try { + const stat = await fsPromises.stat(lockPath) + if (performance.now() - stat.mtime.getTime() > mtimeLimit) { + return + } else { + await new Promise(resolve => setTimeout(resolve, options.lockRetryWait)) + } + } catch (e) { + if (e.code === 'ENOENT') { + return + } else { + throw e + } + } + } while(retries <= options.lockRetries) + + throw `lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} (${options.lockRetryWait * options.lockRetries}) milliseconds.` +} + +function setupMTimeEditor(lockPath: string): () => Promise { + const interval = setInterval(async () => { + try { + const time = new Date(); + await fsPromises.utimes(lockPath, time, time) + } catch {} + }, mtimeUpdateIntervalTime) + + return async () => { + clearInterval(interval) + await fsPromises.rmdir(lockPath) + } +} + +export async function lockFile(path: string): Promise<() => Promise> { + const lockPath = `${path}.lock` + try { + await fsPromises.mkdir(lockPath) + return setupMTimeEditor(lockPath) + } catch (e) { + if (e.code === 'EEXIST') { try { - const locked = checkSync(path, {realpath: false}); - if (!locked) { - return resolve() + const stat = await fsPromises.stat(lockPath) + if (performance.now() - stat.mtime.getTime() > mtimeLimit) { + return setupMTimeEditor(lockPath) } else { - await new Promise(resolve => setTimeout(resolve, options.lockRetryWait)) + throw 'LOCKED' } } catch (e) { - return reject(e) + if (e.code === 'ENOENT') { + //This will run if the lock gets released after the EEXIST error is thrown but before the stat is checked. + //If this is the case, the lock acquisition should be retried. + return await lockFile(path) + } else { + throw e + } } + } else { + throw e } - reject(`lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} milliseconds.`) - }) + } } \ No newline at end of file diff --git a/tests/versions.test.ts b/tests/versions.test.ts index 431e3f11..5d987254 100644 --- a/tests/versions.test.ts +++ b/tests/versions.test.ts @@ -17,7 +17,7 @@ jest.setTimeout(500_000); for (const version of versions) { for (const username of usernames) { - test(`running on version ${version} with username ${username}`, async () => { + test.concurrent(`running on version ${version} with username ${username}`, async () => { Error.stackTraceLimit = Infinity const options: ServerOptions = { version,