Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,69 @@ export class Config {
return null;
}

/**
* Watch providers.jsonc for external edits. Fires callback (debounced 300 ms)
* on any create/modify/delete event. Returns a cleanup function.
*
* We watch the parent directory rather than the file directly so that
* creates (first-time manual edit) are also detected on all platforms.
*/
watchProvidersFile(callback: () => void): () => void {
const filename = path.basename(this.providersFile);
let debounceTimer: ReturnType<typeof setTimeout> | null = null;

const fire = (): void => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
debounceTimer = null;
callback();
}, 300);
};

// Anything inside this block can fail in restricted environments:
// - ensurePrivateDirSync: read-only filesystem, unwritable MUX_ROOT
// - fs.watch: ENOENT, network filesystems (NFS/SMB), watch-limit
// exhaustion (ENOSPC on Linux), unsupported virtualized mounts.
// We degrade gracefully in every case: log once, return a no-op
// cleanup, and let the rest of provider config keep working. The UI
// just won't auto-refresh on manual edits in that environment (same
// as the pre-PR behaviour).
let watcher: fs.FSWatcher;
try {
// The mux home directory may not exist on a fresh install. Create it
// so fs.watch doesn't throw ENOENT; the directory being empty is fine.
if (!fs.existsSync(this.rootDir)) {
ensurePrivateDirSync(this.rootDir);
}

// persistent: false so the watcher doesn't prevent the process (or
// Jest) from exiting when nothing else is keeping the event loop alive.
watcher = fs.watch(this.rootDir, { persistent: false }, (_eventType, changedFilename) => {
// changedFilename can be null on some platforms/kernels (notably
// older macOS FSEvents). When we can't tell which file changed,
// assume providers.jsonc might have and let the consumer re-fetch
// — better an extra refresh than a missed one, since this is the
// exact scenario the feature is meant to fix.
if (changedFilename != null && changedFilename !== filename) return;
fire();
});
} catch (error) {
log.warn(
`Could not watch providers.jsonc for external edits (${this.rootDir}); manual edits will require a restart to take effect:`,
error
);
const noop = (): void => {
// Nothing to clean up — watcher setup never completed.
};
return noop;
}

return () => {
if (debounceTimer) clearTimeout(debounceTimer);
watcher.close();
};
}

/**
* Save providers configuration to JSONC file
* @param config The providers configuration to save
Expand Down
14 changes: 14 additions & 0 deletions src/node/services/providerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export class ProviderService {
private readonly policyService: PolicyService | null;
private readonly emitter = new EventEmitter();
private lastWarnedShadowedCustomProviderIds: Set<string> | null = null;
private readonly stopWatchingProvidersFile: () => void;

constructor(
private readonly config: Config,
Expand All @@ -141,6 +142,19 @@ export class ProviderService {
// The provider config subscription may have many concurrent listeners (e.g. multiple windows).
// Avoid noisy MaxListenersExceededWarning for normal usage.
this.emitter.setMaxListeners(50);
// Notify subscribers when providers.jsonc is edited externally (e.g. manual edits).
this.stopWatchingProvidersFile = this.config.watchProvidersFile(() =>
this.notifyConfigChanged()
);
}

/**
* Release long-lived OS handles (e.g. the providers.jsonc file watcher).
* Called by ServiceContainer.dispose() during shutdown and by tests.
*/
dispose(): void {
this.stopWatchingProvidersFile();
this.emitter.removeAllListeners();
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/node/services/serviceContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ export class ServiceContainer {

this.copilotOauthService.dispose();
this.serverAuthService.dispose();
this.providerService.dispose();
await this.backgroundProcessManager.terminateAll();
}
}