fix: Convert sync file I/O to async in credential handler and account setup
Removes blocking readFileSync, writeFileSync, and mkdirSync calls from the agent spawn hot path, replacing them with async fs/promises equivalents to avoid stalling the Node.js event loop during credential operations.
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
export function setupAccountConfigDir(
|
export async function setupAccountConfigDir(
|
||||||
configDir: string,
|
configDir: string,
|
||||||
extracted: { configJson: object; credentials: string },
|
extracted: { configJson: object; credentials: string },
|
||||||
): void {
|
): Promise<void> {
|
||||||
mkdirSync(configDir, { recursive: true });
|
await mkdir(configDir, { recursive: true });
|
||||||
|
|
||||||
// Write .claude.json
|
// Write .claude.json
|
||||||
writeFileSync(join(configDir, '.claude.json'), JSON.stringify(extracted.configJson, null, 2));
|
await writeFile(join(configDir, '.claude.json'), JSON.stringify(extracted.configJson, null, 2));
|
||||||
|
|
||||||
// Write .credentials.json
|
// Write .credentials.json
|
||||||
writeFileSync(join(configDir, '.credentials.json'), extracted.credentials);
|
await writeFile(join(configDir, '.credentials.json'), extracted.credentials);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* ensuring they're fresh, and marking accounts as exhausted on failure.
|
* ensuring they're fresh, and marking accounts as exhausted on failure.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, existsSync } from 'node:fs';
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import type { AccountRepository } from '../db/repositories/account-repository.js';
|
import type { AccountRepository } from '../db/repositories/account-repository.js';
|
||||||
import type { AccountCredentialManager } from './credentials/types.js';
|
import type { AccountCredentialManager } from './credentials/types.js';
|
||||||
@@ -50,9 +51,9 @@ export class CredentialHandler {
|
|||||||
* Write account credentials from DB to the convention-based config directory.
|
* Write account credentials from DB to the convention-based config directory.
|
||||||
* Must be called before ensureCredentials so the files exist on disk.
|
* Must be called before ensureCredentials so the files exist on disk.
|
||||||
*/
|
*/
|
||||||
writeCredentialsToDisk(account: Account, configDir: string): void {
|
async writeCredentialsToDisk(account: Account, configDir: string): Promise<void> {
|
||||||
if (account.configJson && account.credentials) {
|
if (account.configJson && account.credentials) {
|
||||||
setupAccountConfigDir(configDir, {
|
await setupAccountConfigDir(configDir, {
|
||||||
configJson: JSON.parse(account.configJson),
|
configJson: JSON.parse(account.configJson),
|
||||||
credentials: account.credentials,
|
credentials: account.credentials,
|
||||||
});
|
});
|
||||||
@@ -70,7 +71,7 @@ export class CredentialHandler {
|
|||||||
if (!this.accountRepository) return;
|
if (!this.accountRepository) return;
|
||||||
try {
|
try {
|
||||||
const credPath = join(configDir, '.credentials.json');
|
const credPath = join(configDir, '.credentials.json');
|
||||||
const credentials = readFileSync(credPath, 'utf-8');
|
const credentials = await readFile(credPath, 'utf-8');
|
||||||
await this.accountRepository.updateCredentials(accountId, credentials);
|
await this.accountRepository.updateCredentials(accountId, credentials);
|
||||||
log.debug({ accountId }, 'persisted refreshed credentials back to DB');
|
log.debug({ accountId }, 'persisted refreshed credentials back to DB');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -97,11 +98,11 @@ export class CredentialHandler {
|
|||||||
* Returns null if credentials file is missing or malformed.
|
* Returns null if credentials file is missing or malformed.
|
||||||
* Used for CLAUDE_CODE_OAUTH_TOKEN env var injection.
|
* Used for CLAUDE_CODE_OAUTH_TOKEN env var injection.
|
||||||
*/
|
*/
|
||||||
readAccessToken(configDir: string): string | null {
|
async readAccessToken(configDir: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const credPath = join(configDir, '.credentials.json');
|
const credPath = join(configDir, '.credentials.json');
|
||||||
if (!existsSync(credPath)) return null;
|
if (!existsSync(credPath)) return null;
|
||||||
const raw = readFileSync(credPath, 'utf-8');
|
const raw = await readFile(credPath, 'utf-8');
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
return parsed.claudeAiOauth?.accessToken ?? null;
|
return parsed.claudeAiOauth?.accessToken ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -126,7 +127,7 @@ export class CredentialHandler {
|
|||||||
accountConfigDir = getAccountConfigDir(this.workspaceRoot, accountId);
|
accountConfigDir = getAccountConfigDir(this.workspaceRoot, accountId);
|
||||||
const account = await this.accountRepository.findById(accountId);
|
const account = await this.accountRepository.findById(accountId);
|
||||||
if (account) {
|
if (account) {
|
||||||
this.writeCredentialsToDisk(account, accountConfigDir);
|
await this.writeCredentialsToDisk(account, accountConfigDir);
|
||||||
}
|
}
|
||||||
processEnv[provider.configDirEnv] = accountConfigDir;
|
processEnv[provider.configDirEnv] = accountConfigDir;
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ export class CredentialHandler {
|
|||||||
await this.persistRefreshedCredentials(accountId, accountConfigDir);
|
await this.persistRefreshedCredentials(accountId, accountConfigDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = this.readAccessToken(accountConfigDir);
|
const accessToken = await this.readAccessToken(accountConfigDir);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
processEnv['CLAUDE_CODE_OAUTH_TOKEN'] = accessToken;
|
processEnv['CLAUDE_CODE_OAUTH_TOKEN'] = accessToken;
|
||||||
log.debug({ accountId }, 'CLAUDE_CODE_OAUTH_TOKEN injected');
|
log.debug({ accountId }, 'CLAUDE_CODE_OAUTH_TOKEN injected');
|
||||||
@@ -191,7 +192,7 @@ export class CredentialHandler {
|
|||||||
|
|
||||||
// Write credentials and ensure they're fresh
|
// Write credentials and ensure they're fresh
|
||||||
const nextConfigDir = getAccountConfigDir(this.workspaceRoot, nextAccount.id);
|
const nextConfigDir = getAccountConfigDir(this.workspaceRoot, nextAccount.id);
|
||||||
this.writeCredentialsToDisk(nextAccount, nextConfigDir);
|
await this.writeCredentialsToDisk(nextAccount, nextConfigDir);
|
||||||
const { valid, refreshed } = await this.ensureCredentials(nextConfigDir, nextAccount.id);
|
const { valid, refreshed } = await this.ensureCredentials(nextConfigDir, nextAccount.id);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
log.warn({ newAccountId: nextAccount.id }, 'failed to refresh failover account credentials');
|
log.warn({ newAccountId: nextAccount.id }, 'failed to refresh failover account credentials');
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
accountId = accountResult.accountId;
|
accountId = accountResult.accountId;
|
||||||
accountConfigDir = accountResult.configDir;
|
accountConfigDir = accountResult.configDir;
|
||||||
|
|
||||||
this.credentialHandler.writeCredentialsToDisk(accountResult.account, accountConfigDir);
|
await this.credentialHandler.writeCredentialsToDisk(accountResult.account, accountConfigDir);
|
||||||
const { valid, refreshed } = await this.credentialHandler.ensureCredentials(accountConfigDir, accountId);
|
const { valid, refreshed } = await this.credentialHandler.ensureCredentials(accountConfigDir, accountId);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
log.warn({ alias, accountId }, 'failed to refresh account credentials, proceeding anyway');
|
log.warn({ alias, accountId }, 'failed to refresh account credentials, proceeding anyway');
|
||||||
|
|||||||
Reference in New Issue
Block a user