mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 18:15:23 +00:00
fix(image-analysis): handle installer lock contention
This commit is contained in:
1 parent
77b488f8b1
commit
afc8b0bb6e
2 files changed
+57
-1
No files matched your search
@@ -121,7 +121,7 @@ function writeClaudeUserConfig(configPath: string, config: ClaudeUserConfig): bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withClaudeUserConfigLock<T>(configPath: string, callback: () => T): T {
|
function withClaudeUserConfigLock<T>(configPath: string, callback: () => T): T {
|
||||||
const lockTarget = fs.existsSync(configPath) ? configPath : path.dirname(configPath);
|
const lockTarget = path.dirname(configPath);
|
||||||
let release: (() => void) | undefined;
|
let release: (() => void) | undefined;
|
||||||
|
|
||||||
if (!fs.existsSync(path.dirname(configPath))) {
|
if (!fs.existsSync(path.dirname(configPath))) {
|
||||||
@@ -142,6 +142,11 @@ function withClaudeUserConfigLock<T>(configPath: string, callback: () => T): T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLockUnavailableError(error: unknown): boolean {
|
||||||
|
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
||||||
|
return code === 'ELOCKED' || code === 'ENOTACQUIRED';
|
||||||
|
}
|
||||||
|
|
||||||
export function hasImageAnalysisMcpServerInstalled(): boolean {
|
export function hasImageAnalysisMcpServerInstalled(): boolean {
|
||||||
return (
|
return (
|
||||||
fs.existsSync(getImageAnalysisMcpServerPath()) &&
|
fs.existsSync(getImageAnalysisMcpServerPath()) &&
|
||||||
@@ -183,6 +188,7 @@ function removeManagedServerConfig(configPath: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
return withClaudeUserConfigLock(configPath, () => {
|
return withClaudeUserConfigLock(configPath, () => {
|
||||||
const config = readClaudeUserConfig(configPath);
|
const config = readClaudeUserConfig(configPath);
|
||||||
if (config === null) {
|
if (config === null) {
|
||||||
@@ -229,6 +235,19 @@ function removeManagedServerConfig(configPath: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (isLockUnavailableError(error)) {
|
||||||
|
if (process.env.CCS_DEBUG) {
|
||||||
|
console.error(
|
||||||
|
warn(
|
||||||
|
`Image Analysis MCP cleanup skipped because ${configPath} is locked by another process`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installImageAnalysisMcpServer(): boolean {
|
export function installImageAnalysisMcpServer(): boolean {
|
||||||
@@ -331,6 +350,7 @@ export function ensureImageAnalysisMcpConfig(): boolean {
|
|||||||
env: {},
|
env: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
return withClaudeUserConfigLock(claudeUserConfigPath, () => {
|
return withClaudeUserConfigLock(claudeUserConfigPath, () => {
|
||||||
const config = readClaudeUserConfig(claudeUserConfigPath);
|
const config = readClaudeUserConfig(claudeUserConfigPath);
|
||||||
|
|
||||||
@@ -377,6 +397,19 @@ export function ensureImageAnalysisMcpConfig(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (isLockUnavailableError(error)) {
|
||||||
|
if (process.env.CCS_DEBUG) {
|
||||||
|
console.error(
|
||||||
|
warn(
|
||||||
|
`Image Analysis MCP provisioning skipped because ${claudeUserConfigPath} is locked by another process`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureImageAnalysisMcp(): boolean {
|
export function ensureImageAnalysisMcp(): boolean {
|
||||||
|
|||||||
@@ -184,10 +184,33 @@ describe('ensureImageAnalysisMcp', () => {
|
|||||||
it('serializes ~/.claude.json updates with a file lock', () => {
|
it('serializes ~/.claude.json updates with a file lock', () => {
|
||||||
setupTempHome();
|
setupTempHome();
|
||||||
writeEnabledConfig();
|
writeEnabledConfig();
|
||||||
|
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
|
||||||
|
fs.writeFileSync(claudeUserConfigPath, '{}\n', 'utf8');
|
||||||
|
|
||||||
const lockSpy = spyOn(lockfile, 'lockSync');
|
const lockSpy = spyOn(lockfile, 'lockSync');
|
||||||
|
|
||||||
expect(ensureImageAnalysisMcp()).toBe(true);
|
expect(ensureImageAnalysisMcp()).toBe(true);
|
||||||
expect(lockSpy).toHaveBeenCalled();
|
expect(lockSpy).toHaveBeenCalled();
|
||||||
|
expect(lockSpy.mock.calls[0]?.[0]).toBe(tempHome as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false instead of throwing when ~/.claude.json is already locked', () => {
|
||||||
|
setupTempHome();
|
||||||
|
writeEnabledConfig();
|
||||||
|
|
||||||
|
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
|
||||||
|
fs.writeFileSync(claudeUserConfigPath, '{}\n', 'utf8');
|
||||||
|
|
||||||
|
const release = lockfile.lockSync(tempHome as string, { stale: 10000 }) as () => void;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result: boolean | undefined;
|
||||||
|
expect(() => {
|
||||||
|
result = ensureImageAnalysisMcp();
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user