import isDev from 'electron-is-dev'; import path from 'path'; import { spawn } from 'child_process'; import fs from 'fs-extra'; import ps from 'node:process'; import proxyServer from './proxy-server.js'; import tcpPortUsed from 'tcp-port-used'; import { fileURLToPath, pathToFileURL } from 'url'; import { getPkcDataPath } from './pkc-paths.js'; const dirname = path.join(path.dirname(fileURLToPath(import.meta.url))); const projectRoot = path.join(dirname, '..'); // Get platform-specific binary name const getIpfsBinaryName = () => (process.platform === 'win32' ? 'ipfs.exe' : 'ipfs'); // Get platform subdirectory name for bin/ folder const getPlatformDir = () => { if (process.platform === 'win32') return 'win'; if (process.platform === 'darwin') return 'mac'; return 'linux'; }; const getBundledKuboPath = (rootPath) => path.join(rootPath, 'bin', getPlatformDir(), getIpfsBinaryName()); const downloadBundledIpfsClients = async () => { // before-pack.js is excluded from packaged builds, so only load it in dev. const { downloadIpfsClients } = await import('./before-pack.js'); await downloadIpfsClients(); }; // Resolve kubo binary path const getKuboPath = async () => { if (isDev) { if (process.env.KUBO_BINARY) { return process.env.KUBO_BINARY; } const bundledKuboPath = getBundledKuboPath(path.join(dirname, '..')); if (fs.existsSync(bundledKuboPath)) { return bundledKuboPath; } console.log(`Kubo binary missing at ${bundledKuboPath}, downloading repo-managed binary...`); await downloadBundledIpfsClients(); if (fs.existsSync(bundledKuboPath)) { return bundledKuboPath; } throw new Error(`Kubo binary download completed but '${bundledKuboPath}' was not found`); } else { // In production, the binary is downloaded to bin//ipfs by generateAssets hook // With asar: false, files are at resources/app/ instead of resources/app.asar.unpacked const appPath = process.resourcesPath; const binaryName = getIpfsBinaryName(); // Try the bin/ directory first (where generateAssets downloads binaries) const binDirPath = getBundledKuboPath(path.join(appPath, 'app')); if (fs.existsSync(binDirPath)) { return binDirPath; } // Fallback: try app.asar.unpacked for ASAR builds (if we ever re-enable ASAR) const unpackedPath = path.join(appPath, 'app.asar.unpacked'); const kuboModulePath = path.join(unpackedPath, 'node_modules', 'kubo'); // Try to import kubo from unpacked location try { const kuboUrl = pathToFileURL(path.resolve(kuboModulePath)).href; const kuboModule = await import(kuboUrl); const { path: getKuboBinaryPath } = kuboModule; return getKuboBinaryPath(); } catch (err) { // Fallback: try to find the binary directly in kubo module const kuboBinPath = path.join(kuboModulePath, 'kubo', binaryName); if (fs.existsSync(kuboBinPath)) { return kuboBinPath; } // Last resort: check in resources/app/node_modules/kubo for non-ASAR builds const appModulePath = path.join(appPath, 'app', 'node_modules', 'kubo'); const appKuboBinPath = path.join(appModulePath, 'kubo', binaryName); if (fs.existsSync(appKuboBinPath)) { return appKuboBinPath; } throw new Error(`Could not find kubo binary. Checked: ${binDirPath}, ${kuboBinPath}, ${appKuboBinPath}`); } } }; // use this custom function instead of spawnSync for better logging // also spawnSync might have been causing crash on start on windows const spawnAsync = (...args) => new Promise((resolve, reject) => { const spawnedProcess = spawn(...args); spawnedProcess.on('exit', (exitCode, signal) => { if (exitCode === 0) resolve(); else reject(Error(`spawnAsync process '${spawnedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`)); }); // Always surface errors from short-lived commands spawnedProcess.stderr.on('data', (data) => console.error(data.toString())); // Short-lived command stdout can be useful in dev, but is noisy in prod if (isDev) { spawnedProcess.stdout.on('data', (data) => console.log(data.toString())); } else { // Drain to avoid backpressure without logging spawnedProcess.stdout.on('data', () => {}); } spawnedProcess.on('error', (data) => console.error(data.toString?.() || String(data))); }); const startIpfs = async () => { const ipfsPath = await getKuboPath(); const ipfsDataPath = path.join(getPkcDataPath({ isDev, projectRoot }), 'ipfs'); if (!fs.existsSync(ipfsPath)) { throw Error(`ipfs binary '${ipfsPath}' doesn't exist`); } console.log({ ipfsPath, ipfsDataPath }); fs.ensureDirSync(ipfsDataPath); // Reduce IPFS daemon log verbosity in production to avoid UI lag from excessive logging const env = { ...process.env, IPFS_PATH: ipfsDataPath, ...(isDev ? {} : { GOLOG_LOG_LEVEL: 'error' }) }; // init ipfs client on first launch try { await spawnAsync(ipfsPath, ['init'], { env, hideWindows: true }); } catch {} // make sure repo is migrated try { await spawnAsync(ipfsPath, ['repo', 'migrate'], { env, hideWindows: true }); } catch {} // dont use 8080 port because it's too common await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', '"/ip4/127.0.0.1/tcp/6473"'], { env, hideWindows: true, }); // use different port with proxy for debugging during env let apiAddress = '/ip4/127.0.0.1/tcp/50019'; if (isDev) { apiAddress = apiAddress.replace('50019', '50029'); proxyServer.start({ proxyPort: 50019, targetPort: 50029 }); } await spawnAsync(ipfsPath, ['config', 'Addresses.API', apiAddress], { env, hideWindows: true }); const startIpfsDaemon = () => new Promise((resolve, reject) => { const ipfsProcess = spawn(ipfsPath, ['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'], { env, hideWindows: true }); console.log(`ipfs daemon process started with pid ${ipfsProcess.pid}`); let lastError; ipfsProcess.stderr.on('data', (data) => { lastError = data.toString(); if (isDev) console.error(lastError); }); ipfsProcess.stdout.on('data', (chunk) => { const text = chunk.toString(); if (isDev) console.log(text); if (text.includes('Daemon is ready')) { resolve(); } }); ipfsProcess.on('error', (err) => console.error(err?.toString?.() || String(err))); ipfsProcess.on('exit', () => { console.error(`ipfs process with pid ${ipfsProcess.pid} exited`); reject(Error(lastError)); }); process.on('exit', () => { try { ps.kill(ipfsProcess.pid); } catch (e) { console.log(e); } try { // sometimes ipfs doesnt exit unless we kill pid +1 ps.kill(ipfsProcess.pid + 1); } catch (e) { console.log(e); } }); }); await startIpfsDaemon(); }; const DefaultExport = {}; export default DefaultExport; const startIpfsAutoRestart = async () => { let pendingStart = false; const start = async () => { if (pendingStart) { return; } pendingStart = true; try { const started = await tcpPortUsed.check(isDev ? 50029 : 50019, '127.0.0.1'); if (!started) { await startIpfs(); } } catch (e) { console.log('failed starting ipfs', e); try { // try to run exported onError callback, can be undefined DefaultExport.onError(e)?.catch?.(console.log); } catch {} } pendingStart = false; }; // retry starting ipfs every 1 second, // in case it was started by another client that shut down and shut down ipfs with it start(); setInterval(() => { start(); }, 1000); }; startIpfsAutoRestart();