Files
5chan/electron/start-ipfs.js
T

221 lines
7.7 KiB
JavaScript

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 EnvPaths from 'env-paths';
import { fileURLToPath, pathToFileURL } from 'url';
const dirname = path.join(path.dirname(fileURLToPath(import.meta.url)));
const envPaths = EnvPaths('plebbit', { suffix: false });
// 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/<platform>/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 = isDev ? path.join(dirname, '..', '.plebbit', 'ipfs') : path.join(envPaths.data, '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();