mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 14:23:02 -07:00
fix(core): fix ShellExecutionConfig spread and add ProjectRegistry save backoff
This commit is contained in:
@@ -3340,20 +3340,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.shellExecutionConfig;
|
||||
}
|
||||
|
||||
setShellExecutionConfig(config: ShellExecutionConfig): void {
|
||||
setShellExecutionConfig(config: Partial<ShellExecutionConfig>): void {
|
||||
this.shellExecutionConfig = {
|
||||
...this.shellExecutionConfig,
|
||||
terminalWidth:
|
||||
config.terminalWidth ?? this.shellExecutionConfig.terminalWidth,
|
||||
terminalHeight:
|
||||
config.terminalHeight ?? this.shellExecutionConfig.terminalHeight,
|
||||
showColor: config.showColor ?? this.shellExecutionConfig.showColor,
|
||||
pager: config.pager ?? this.shellExecutionConfig.pager,
|
||||
sanitizationConfig:
|
||||
config.sanitizationConfig ??
|
||||
this.shellExecutionConfig.sanitizationConfig,
|
||||
sandboxManager:
|
||||
config.sandboxManager ?? this.shellExecutionConfig.sandboxManager,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
getScreenReader(): boolean {
|
||||
|
||||
@@ -300,4 +300,36 @@ describe('ProjectRegistry', () => {
|
||||
'ProjectRegistry must be initialized before use',
|
||||
);
|
||||
});
|
||||
|
||||
it('retries on EBUSY during save', async () => {
|
||||
const registry = new ProjectRegistry(registryPath);
|
||||
await registry.initialize();
|
||||
|
||||
const renameSpy = vi.spyOn(fs.promises, 'rename');
|
||||
let ebusyCount = 0;
|
||||
|
||||
renameSpy.mockImplementation(async (oldPath, newPath) => {
|
||||
// Only throw for the specific temporary file generated by save()
|
||||
if (oldPath.toString().includes('.tmp') && ebusyCount < 2) {
|
||||
ebusyCount++;
|
||||
const err = new Error('Resource busy or locked');
|
||||
(err as { code?: string }).code = 'EBUSY';
|
||||
throw err;
|
||||
}
|
||||
return fs.promises
|
||||
.copyFile(oldPath, newPath)
|
||||
.then(() => fs.promises.unlink(oldPath));
|
||||
});
|
||||
|
||||
const projectPath = path.join(tempDir, 'ebusy-project');
|
||||
const shortId = await registry.getShortId(projectPath);
|
||||
expect(shortId).toBe('ebusy-project');
|
||||
expect(ebusyCount).toBe(2);
|
||||
|
||||
// Verify it actually saved properly after retries
|
||||
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
||||
expect(data.projects[normalizePath(projectPath)]).toBe('ebusy-project');
|
||||
|
||||
renameSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { lock } from 'proper-lockfile';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
|
||||
export interface RegistryData {
|
||||
projects: Record<string, string>;
|
||||
@@ -83,17 +84,48 @@ export class ProjectRegistry {
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const tmpPath = this.registryPath + '.' + randomUUID() + '.tmp';
|
||||
try {
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
// Use a randomized tmp path to avoid ENOENT crashes when save() is called concurrently
|
||||
const tmpPath = this.registryPath + '.' + randomUUID() + '.tmp';
|
||||
await fs.promises.writeFile(tmpPath, content, 'utf8');
|
||||
await fs.promises.rename(tmpPath, this.registryPath);
|
||||
|
||||
// Exponential backoff for OS-level file locks (EBUSY/EPERM) during rename
|
||||
const maxRetries = 5;
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
await fs.promises.rename(tmpPath, this.registryPath);
|
||||
break; // Success
|
||||
} catch (error: unknown) {
|
||||
const code = isNodeError(error) ? error.code : '';
|
||||
|
||||
if (
|
||||
(code === 'EBUSY' || code === 'EPERM') &&
|
||||
attempt < maxRetries - 1
|
||||
) {
|
||||
const delayMs = Math.pow(2, attempt) * 50;
|
||||
debugLogger.debug(
|
||||
`Rename failed with ${code}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})...`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.error(
|
||||
`Failed to save project registry to ${this.registryPath}:`,
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
// Clean up the temporary file if it was left behind
|
||||
try {
|
||||
if (fs.existsSync(tmpPath)) {
|
||||
await fs.promises.unlink(tmpPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user