diff --git a/src/config/configFile.ts b/src/config/configFile.ts index 32c1302bb..3b7e94c54 100644 --- a/src/config/configFile.ts +++ b/src/config/configFile.ts @@ -14,7 +14,7 @@ import { Global } from '../global'; import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { resolveProjectPath, resolveProjectPathSync } from '../util/internal'; -import { lockInit, lockInitSync } from '../util/fileLocking'; +import { lockInit, lockInitSync, pollUntilUnlock, pollUntilUnlockSync } from '../util/fileLocking'; import { BaseConfigStore } from './configStore'; import { ConfigContents } from './configStackTypes'; import { stateFromContents } from './lwwMap'; @@ -167,7 +167,7 @@ export class ConfigFile< !this.hasRead ? 'hasRead is false' : 'force parameter is true' }` ); - + await pollUntilUnlock(this.getPath()); const obj = parseJsonMap

(await fs.promises.readFile(this.getPath(), 'utf8'), this.getPath()); this.setContentsFromFileContents(obj, (await fs.promises.stat(this.getPath(), { bigint: true })).mtimeNs); } @@ -203,6 +203,7 @@ export class ConfigFile< // Only need to read config files once. They are kept up to date // internally and updated persistently via write(). if (!this.hasRead || force) { + pollUntilUnlockSync(this.getPath()); this.logger.debug(`Reading config file: ${this.getPath()}`); const obj = parseJsonMap

(fs.readFileSync(this.getPath(), 'utf8')); this.setContentsFromFileContents(obj, fs.statSync(this.getPath(), { bigint: true }).mtimeNs); diff --git a/src/util/fileLocking.ts b/src/util/fileLocking.ts index db955221b..76bab6005 100644 --- a/src/util/fileLocking.ts +++ b/src/util/fileLocking.ts @@ -6,9 +6,12 @@ */ import * as fs from 'node:fs'; import { dirname } from 'node:path'; -import { lock, lockSync } from 'proper-lockfile'; +import { lock, lockSync, check, checkSync } from 'proper-lockfile'; +import { Duration } from '@salesforce/kit'; import { SfError } from '../sfError'; import { Logger } from '../logger/logger'; +import { PollingClient } from '../status/pollingClient'; +import { StatusResult } from '../status/types'; import { lockOptions, lockRetryOptions } from './lockRetryOptions'; type LockInitResponse = { writeAndUnlock: (data: string) => Promise; unlock: () => Promise }; @@ -95,3 +98,55 @@ export const lockInitSync = (filePath: string): LockInitSyncResponse => { unlock, }; }; + +/** + * Poll until the file is unlocked. + * + * @param filePath file path to check + */ +export const pollUntilUnlock = async (filePath: string): Promise => { + const options: PollingClient.Options = { + async poll(): Promise { + try { + const locked = await check(filePath, lockRetryOptions); + return { completed: !locked, payload: 'File unlocked' }; + } catch (e) { + if (e instanceof SfError) { + return { completed: true, payload: e.toObject() }; + } + if (e instanceof Error) { + return { + completed: true, + payload: { + name: e.name, + message: e.message, + stack: e.stack, + }, + }; + } + + return { completed: true, payload: 'Error occurred' }; + } + }, + frequency: Duration.milliseconds(10), + timeout: Duration.minutes(1), + }; + + const client = await PollingClient.create(options); + await client.subscribe(); +}; + +export const pollUntilUnlockSync = (filePath: string): void => { + // Set a counter to ensure that the while loop does not run indefinitely + let counter = 0; + let locked = true; + while (locked && counter < 100) { + try { + locked = checkSync(filePath, lockOptions); + counter++; + } catch { + // Likely a file not found error, which means the file is not locked + locked = false; + } + } +};