mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Support locking cache file when writing and handles syntax error when reading (#21469)
This commit is contained in:
@@ -4,42 +4,62 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICachePlugin, TokenCacheContext } from '@azure/msal-node';
|
||||
import { constants, promises as fsPromises } from 'fs';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
|
||||
import * as lockFile from 'lockfile';
|
||||
import * as path from 'path';
|
||||
import { AccountsClearTokenCacheCommand } from '../../constants';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
|
||||
export class MsalCachePluginProvider {
|
||||
constructor(
|
||||
private readonly _serviceName: string,
|
||||
private readonly _msalFilePath: string
|
||||
private readonly _msalFilePath: string,
|
||||
) {
|
||||
this._msalFilePath = path.join(this._msalFilePath, this._serviceName);
|
||||
this._serviceName = this._serviceName.replace(/-/, '_');
|
||||
Logger.verbose(`MsalCachePluginProvider: Using cache path ${_msalFilePath} and serviceName ${_serviceName}`);
|
||||
}
|
||||
|
||||
private _lockTaken: boolean = false;
|
||||
|
||||
private getLockfilePath(): string {
|
||||
return this._msalFilePath + '.lock';
|
||||
}
|
||||
|
||||
public getCachePlugin(): ICachePlugin {
|
||||
const lockFilePath = this.getLockfilePath();
|
||||
const beforeCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
|
||||
let exists = true;
|
||||
await this.waitAndLock(lockFilePath);
|
||||
try {
|
||||
await fsPromises.access(this._msalFilePath, constants.R_OK | constants.W_OK);
|
||||
} catch {
|
||||
exists = false;
|
||||
}
|
||||
if (exists) {
|
||||
const cache = await fsPromises.readFile(this._msalFilePath, { encoding: 'utf8' });
|
||||
try {
|
||||
const cache = await fsPromises.readFile(this._msalFilePath, { encoding: 'utf8' });
|
||||
cacheContext.tokenCache.deserialize(cache);
|
||||
Logger.verbose(`MsalCachePlugin: Token read from cache successfully.`);
|
||||
} catch (e) {
|
||||
Logger.error(`MsalCachePlugin: Failed to read from cache file. ${e}`);
|
||||
// Handle deserialization error in cache file in case file gets corrupted.
|
||||
// Clearing cache here will ensure account is marked stale so re-authentication can be triggered.
|
||||
Logger.verbose(`MsalCachePlugin: Error occurred when trying to read cache file, file contents will be cleared: ${e.message}`);
|
||||
await fsPromises.writeFile(this._msalFilePath, '', { encoding: 'utf8' });
|
||||
}
|
||||
Logger.verbose(`MsalCachePlugin: Token read from cache successfully.`);
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
// File doesn't exist, log and continue
|
||||
Logger.verbose(`MsalCachePlugin: Cache file not found on disk: ${e.code}`);
|
||||
}
|
||||
else {
|
||||
Logger.error(`MsalCachePlugin: Failed to read from cache file: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
lockFile.unlockSync(lockFilePath);
|
||||
this._lockTaken = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const afterCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
|
||||
if (cacheContext.cacheHasChanged) {
|
||||
await this.waitAndLock(lockFilePath);
|
||||
try {
|
||||
const data = cacheContext.tokenCache.serialize();
|
||||
await fsPromises.writeFile(this._msalFilePath, data, { encoding: 'utf8' });
|
||||
@@ -47,6 +67,9 @@ export class MsalCachePluginProvider {
|
||||
} catch (e) {
|
||||
Logger.error(`MsalCachePlugin: Failed to write to cache file. ${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
lockFile.unlockSync(lockFilePath);
|
||||
this._lockTaken = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -61,4 +84,36 @@ export class MsalCachePluginProvider {
|
||||
afterCacheAccess,
|
||||
};
|
||||
}
|
||||
|
||||
private async waitAndLock(lockFilePath: string): Promise<void> {
|
||||
// Make 500 retry attempts with 100ms wait time between each attempt to allow enough time for the lock to be released.
|
||||
const retries = 500;
|
||||
const retryWait = 100;
|
||||
|
||||
// We cannot rely on lockfile.lockSync() to clear stale lockfile,
|
||||
// so we check if the lockfile exists and if it does, calling unlockSync() will clear it.
|
||||
if (lockFile.checkSync(lockFilePath) && !this._lockTaken) {
|
||||
lockFile.unlockSync(lockFilePath);
|
||||
Logger.verbose(`MsalCachePlugin: Stale lockfile found and has been removed.`);
|
||||
}
|
||||
|
||||
let retryAttempt = 0;
|
||||
while (retryAttempt <= retries) {
|
||||
try {
|
||||
// Use lockfile.lockSync() to ensure only one process is accessing the cache at a time.
|
||||
// lockfile.lock() does not wait for async callback promise to resolve.
|
||||
lockFile.lockSync(lockFilePath);
|
||||
this._lockTaken = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
if (retryAttempt === retries) {
|
||||
Logger.error(`MsalCachePlugin: Failed to acquire lock on cache file after ${retries} attempts.`);
|
||||
throw new Error(`Failed to acquire lock on cache file after ${retries} attempts. Please attempt command: '${AccountsClearTokenCacheCommand}' to clear access token cache.`);
|
||||
}
|
||||
retryAttempt++;
|
||||
Logger.verbose(`MsalCachePlugin: Failed to acquire lock on cache file. Retrying in ${retryWait} ms.`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryWait));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user