import { fs, app, installations, settings, sourceMap, detectDesktopInstall, isGitAvailable, tryConfigureBootstrapPygit2, tryConfigurePygit2Fallback, createCache, fetchJSON, getLatestStableTag, getStableTags, setCallbacks, _broadcastToRenderer, migrateDefaults, checkInstallationUpdates, installDirStateAsync, refreshInstallDirStates, sweepOrphanPartitions, UPDATE_CHECK_INTERVAL } from './shared' import { R2_BASE_URL } from '../r2Mirror' import * as releaseCache from '../release-cache' import type { RegisterCallbacks } from './shared' import { registerAppHandlers } from './registerAppHandlers' import { registerInstallationHandlers } from './registerInstallationHandlers' import { registerSnapshotHandlers } from './registerSnapshotHandlers' import { registerSettingsHandlers } from './registerSettingsHandlers' import { registerSessionHandlers } from './registerSessionHandlers' import { registerTerminalHandlers } from './registerTerminalHandlers' import { setTerminalEnvResolver } from '../terminal' import { registerLogsHandlers } from './registerLogsHandlers' import { registerCrashHandlers } from './registerCrashHandlers' import { registerTelemetryHandlers } from './registerTelemetryHandlers' import { registerDevPlatformHandlers } from './registerDevPlatformHandlers' import { registerEmbeddedSessionHandlers } from './registerEmbeddedSessionHandlers' import { reconcileAdoptedSettings } from '../desktopAdopt' import { finalizeComfyBuilderRecovery, recoverComfyBuilderInstallation } from '../../sources/comfybuilder' export { getAppVersion, stopRunning, cancelLaunching, hasRunningSessions, getSessionProcess, hasActiveOperations, getActiveDetails, cancelAll } from './shared' export type { RegisterCallbacks, ExitCallbackInfo } from './shared' // Idempotent guard so a re-run (tests/hot-reload) doesn't double-subscribe. let _releaseCacheBridgeWired = false function wireReleaseCacheBroadcast(): void { if (_releaseCacheBridgeWired) return _releaseCacheBridgeWired = true releaseCache.onEnriched((repo) => { _broadcastToRenderer('release-cache-enriched', { repo }) }) } export function register(callbacks: RegisterCallbacks = {}): Promise { setCallbacks(callbacks) wireReleaseCacheBroadcast() // These startup writes go through loadForWrite(), which rejects when // installations.json cannot be safely modified right now (locked or corrupt, // issue #1367) - log and continue instead of an unhandled rejection. const warnStartupWrite = (op: string) => (err: unknown) => { console.warn(`[ipc] Skipped startup installations ${op}:`, err) } installations .seedDefaults([ { name: installations.CLOUD_INSTALL_NAME, sourceId: installations.CLOUD_SOURCE_ID, remoteUrl: 'https://cloud.comfy.org/', launchMode: 'window', browserPartition: 'shared' } ]) .catch(warnStartupWrite('seedDefaults')) installations .ensureExists(installations.CLOUD_SOURCE_ID, { name: installations.CLOUD_INSTALL_NAME, sourceId: installations.CLOUD_SOURCE_ID, remoteUrl: 'https://cloud.comfy.org/', launchMode: 'window', browserPartition: 'shared', status: 'installed' }) .catch(warnStartupWrite('ensureExists(cloud)')) // The Cloud entry is not user-renamable; reset any entry a prior build // let the user rename back to the canonical name (issue #922). Runs after // ensureExists via the shared FIFO write queue. installations.enforceCloudName().catch(warnStartupWrite('enforceCloudName')) // Auto-track a detected Legacy Desktop install. { const desktopInfo = detectDesktopInstall() if (desktopInfo) { installations .ensureExists('desktop', { name: 'ComfyUI Legacy Desktop', sourceId: 'desktop', installPath: desktopInfo.basePath, launchMode: 'external', desktopExePath: desktopInfo.executablePath || undefined, status: 'installed' }) .catch(warnStartupWrite('ensureExists(desktop)')) } } const startupMaintenance = migrateDefaults() .then(async () => { await reconcileAdoptedSettings() }) .catch((err) => { console.warn('[ipc] Failed to migrate startup defaults or adopted settings:', err) }) const startupRecovery = startupMaintenance.then(async () => { try { let all = await installations.list() let recovered = false const recoveredInstallationIds = new Set() for (const inst of all.filter((item) => item.sourceId === 'comfybuilder')) { try { const result = await recoverComfyBuilderInstallation(inst) if (result.action === 'update') { const updated = await installations.update(inst.id, result.data) if (!updated) continue recovered = true recoveredInstallationIds.add(inst.id) await finalizeComfyBuilderRecovery(inst.installPath) } } catch (err) { console.warn(`[ipc] Failed to recover ComfyBuilder install ${inst.id}:`, err) } } if (recovered) all = await installations.list() if (recovered) _broadcastToRenderer('installations-changed', {}) return { all, recoveredInstallationIds } } catch (err) { console.warn('[ipc] Failed to recover ComfyBuilder installations:', err) return null } }) // Sweep leftover empty local install dirs (aborted installs) after recovery. // Only reclaim dirs that exist but are effectively empty - never a missing // or unreadable dir, which would silently forget a tracked instance whose // drive is merely offline/renamed (issue #1155). void startupRecovery.then(async (recovery) => { if (!recovery) return try { const { all, recoveredInstallationIds } = recovery const sweepable = all.filter((inst) => { const source = sourceMap[inst.sourceId] return ( source && !source.skipInstall && inst.installPath && !recoveredInstallationIds.has(inst.id) ) }) // Probe in parallel so a few offline paths don't serialize their timeouts. // Only an existing-but-empty dir (aborted install) is reclaimed; a missing, // access-denied, or timed-out ('inaccessible') dir is kept (issue #1155). const states = await Promise.all( sweepable.map(async (inst) => ({ inst, state: await installDirStateAsync(inst.installPath) })) ) let swept = false for (const { inst, state } of states) { if (state !== 'empty') continue try { fs.rmSync(inst.installPath, { recursive: true, force: true }) } catch {} await installations.remove(inst.id) swept = true } if (swept) _broadcastToRenderer('installations-changed', {}) // Reclaim per-install browser partitions left behind by deletes (the // inline cleanup can't remove them on Windows while their session is // alive). Uses the post-sweep id set so swept installs are reclaimed too. const remaining = await installations.list() sweepOrphanPartitions(new Set(remaining.map((i) => i.id))) } catch {} }) // Default to bundled bootstrap pygit2 so the pygit2 path is always exercised // (system git would otherwise mask bugs real users hit). Falls back to // standalone pygit2 then system git; COMFY_FORCE_BOOTSTRAP_GIT=1 disables that. // // Note: individual network operations (clone/fetch/checkout) additionally // fall back to system git at runtime when pygit2 hits an authentication-class // failure — e.g. a global `insteadOf` https->ssh rewrite the SSH-less bundled // pygit2 can't satisfy. Developers can set COMFY_FORCE_PYGIT2=1 to disable // that per-op fallback (and force the pygit2 backend in ComfyUI-Manager too), // keeping the pygit2 path exercised. See git.ts `withSystemGitFallback`. void (async () => { const configureGitBackend = async (): Promise => { const forceBootstrap = process.env.COMFY_FORCE_BOOTSTRAP_GIT === '1' if (await tryConfigureBootstrapPygit2()) { console.log('[ipc] Using bootstrap pygit2 for git operations (default)') return } console.warn( '[ipc] Bootstrap pygit2 not available — bootstrap-python// is missing. ' + 'Run "pnpm run bootstrap" (or "pnpm run bootstrap:fetch") to build it. ' + 'Falling back to standalone-install pygit2 / system git.' ) if (forceBootstrap) { console.warn( '[ipc] COMFY_FORCE_BOOTSTRAP_GIT set but bootstrap python not found — no git backend will be configured' ) return } // Isolated try/catch so a listing failure doesn't skip the system-git fallback. try { const all = await installations.list() for (const inst of all) { if (inst.sourceId !== 'standalone' || !inst.installPath) continue if (await tryConfigurePygit2Fallback(inst.installPath)) { console.log( '[ipc] Configured pygit2 fallback via standalone install at', inst.installPath ) return } } } catch (err) { console.warn('[ipc] Failed to enumerate standalone installations for pygit2 fallback:', err) } // Final fallback: system git, if installed. try { if (await isGitAvailable()) { console.log( '[ipc] Using system git (bootstrap pygit2 and standalone pygit2 both unavailable)' ) return } } catch (err) { console.warn('[ipc] isGitAvailable() check failed:', err) } console.warn( '[ipc] No git backend available (bootstrap pygit2, standalone pygit2, and system git all missing)' ) } await configureGitBackend() // Pre-warm both stable-tag caches so the New Install wizard is responsive on // first open (no local clone needed): the latest tag drives the concrete // version shown for the stable channel, and the full list backs the version // picker. They use independent caches, so warm both here to move the slow // ls-remote (cold/proxied setups) off the wizard's blocking field cascade. try { await Promise.all([getLatestStableTag(), getStableTags()]) } catch {} })() // Clean up partial downloads void (async () => { try { const cache = createCache( settings.get('cacheDir') as string, settings.get('maxCachedDownloads') as number ) await cache.cleanPartials() } catch {} })() // Pre-warm the ETag cache void (async () => { try { await fetchJSON(`${R2_BASE_URL}/latest.json`) } catch {} })() // Check installation updates on startup and periodically setTimeout(() => checkInstallationUpdates(), 3_000) setInterval(() => checkInstallationUpdates(), UPDATE_CHECK_INTERVAL) // Probe local install dir availability on startup, periodically, and whenever // a window regains focus — the last one clears a stale "directory not found" // pill promptly after the user reconnects a drive / restores a folder without // waiting for the periodic pass. Single-flight + change-only broadcast keep // the frequent focus events cheap. void refreshInstallDirStates() setInterval(() => refreshInstallDirStates(), UPDATE_CHECK_INTERVAL) app.on('browser-window-focus', () => { void refreshInstallDirStates() }) // Register all handler groups registerAppHandlers() registerInstallationHandlers() registerSnapshotHandlers() registerSettingsHandlers() registerSessionHandlers() // Let the Console activate each install's actual environment (git venv, // portable embedded python, …) instead of assuming the standalone layout. setTerminalEnvResolver((inst) => sourceMap[inst.sourceId]?.getTerminalEnv?.(inst) ?? null) registerTerminalHandlers() registerLogsHandlers() registerCrashHandlers() registerTelemetryHandlers() registerDevPlatformHandlers() registerEmbeddedSessionHandlers() return startupRecovery.then(() => undefined) }