diff --git a/.vscode/settings.json b/.vscode/settings.json index cac0e10..d7335b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,12 @@ { - "editor.formatOnSave": true + "editor.formatOnSave": false, + "editor.formatOnPaste": false, + "[javascript]": { + "editor.formatOnSave": false + }, + "editor.codeActionsOnSave": { + "source.fixAll": "never", + "source.fixAll.eslint": "never" + }, + "standard.autoFixOnSave": false } \ No newline at end of file diff --git a/bin/index.js b/bin/index.js index 97a7df9..c96f5cc 100755 --- a/bin/index.js +++ b/bin/index.js @@ -2,11 +2,12 @@ const prog = require('caporal') const register = require('../src/commands') +const package = require('../package.json') prog .bin('shardus-network') .name('Shardus Network') - .version('1.0.0') + .version(package.version) for (const command in register) { register[command](prog) diff --git a/package-lock.json b/package-lock.json index 187eace..c6726ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@shardus/network-tool", - "version": "4.4.0", + "version": "4.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@shardus/network-tool", - "version": "4.4.0", + "version": "4.4.1", "license": "MIT", "dependencies": { "caporal": "^1.3.0", diff --git a/package.json b/package.json index 5116647..23cb08d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shardus/network-tool", - "version": "4.4.0", + "version": "4.4.1", "description": "Tool for managing a Shardus test network", "main": "index.js", "scripts": { diff --git a/src/actions/create.js b/src/actions/create.js index d84d716..7b061f5 100644 --- a/src/actions/create.js +++ b/src/actions/create.js @@ -239,8 +239,9 @@ module.exports = async function (args, options, logger) { await create(networkDir, networkConfig, num, args.pm2) if (!options['noLogRotation']) { - await util.pm2InstallRotateLog(networkDir) - await util.pm2SetRotateLog(networkDir) + const logSize = options.logSizeMb || networkConfig.logSize || defaultNetwork.logSize + const logNum = options.logNum || networkConfig.logNum || defaultNetwork.logNum + await util.pm2SetupLogRotation(networkDir, logSize, logNum) } if (options['noStart'] === false) { start(networkDir, num, 'start', args.pm2) @@ -272,8 +273,7 @@ module.exports = async function (args, options, logger) { await create(networkDir, config) if (!options['noLogRotation']) { - await util.pm2InstallRotateLog(networkDir) - await util.pm2SetRotateLog(networkDir, options.logSizeMb, options.logSize) + await util.pm2SetupLogRotation(networkDir, config.logSize, config.logNum) } if (options['noStart'] === false) { start(networkDir, num, 'create', args.pm2, options) @@ -285,8 +285,7 @@ module.exports = async function (args, options, logger) { inquirer.prompt(questions).then(async (answers) => { await create(networkDir, answers) if (!options['noLogRotation']) { - await util.pm2InstallRotateLog(networkDir) - await util.pm2SetRotateLog(networkDir, answers.logSize, answers.logNum) + await util.pm2SetupLogRotation(networkDir, answers.logSize, answers.logNum) } if (options['noStart'] === false) { start(networkDir, num, 'create', args.pm2, options) diff --git a/src/actions/start.js b/src/actions/start.js index bfafcfd..ae70c3b 100644 --- a/src/actions/start.js +++ b/src/actions/start.js @@ -4,9 +4,19 @@ const util = require('../lib/util') const create = require('../actions/create') const fs = require('fs') -module.exports = function (args, options, logger) { +module.exports = async function (args, options, logger) { try { const networkDir = util.setNetworkDirOrErr(options.dir) + const configPath = path.join(networkDir, 'network-config.json') + + // Setup logrotate if network exists and logrotate not disabled + if (fs.existsSync(configPath) && !options['noLogRotation']) { + const networkConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + const logSize = options.logSizeMb || networkConfig.logSize || 10 + const logNum = options.logNum || networkConfig.logNum || 10 + await util.pm2SetupLogRotation(networkDir, logSize, logNum) + } + start(networkDir, parseInt(args.num), 'create', args.pm2, options) } catch (err) { create(args, Object.assign(options, { noStart: false }), logger) diff --git a/src/lib/restart.js b/src/lib/restart.js index 4c3bc91..90afc0f 100644 --- a/src/lib/restart.js +++ b/src/lib/restart.js @@ -50,18 +50,20 @@ module.exports = async function (networkDir, options, args) { activeArchiversCalculated = true } + const restartPromises = [] for (let i = 0; i < count; i++) { port = stoppedArchivers[i].port - 4000; console.log("archive-server-" + (port + 1)); newArchiversInfo.push({ ip: stoppedArchivers[i].ip, port: stoppedArchivers[i].port, publicKey: stoppedArchivers[i].publicKey }) - await util.pm2Restart(networkDir, `"archive-server-${port + 1}"`, { + restartPromises.push(util.pm2Restart(networkDir, `"archive-server-${port + 1}"`, { ARCHIVER_PORT: stoppedArchivers[i].port, ARCHIVER_PUBLIC_KEY: stoppedArchivers[i].publicKey, ARCHIVER_SECRET_KEY: archiverKeys[port].secretKey, ARCHIVER_INFO: activeArchiversEnv, ARCHIVER_DB: `archiver-db-${stoppedArchivers[i].port}`, - }); + })) } + await Promise.all(restartPromises) } if (existingArchivers.length > 0) { @@ -77,20 +79,22 @@ module.exports = async function (networkDir, options, args) { activeArchiversEnv = activeArchivers.map((archiver) => `${archiver.ip}:${archiver.port}:${archiver.publicKey}`).join(',') activeArchiversCalculated = true } - // Restart archivers on ports following existingArchivers + // Restart archivers on ports following existingArchivers (parallel) + const archiverRestartPromises = [] for (let i = 0; i < restartArchiverCount; i++) { port = existingArchivers[i].port - 4000; console.log("archive-server-" + (port + 1)); newArchiversInfo.push({ ip: existingArchivers[i].ip, port: existingArchivers[i].port, publicKey: existingArchivers[i].publicKey }) - await util.pm2Restart(networkDir, `"archive-server-${port + 1}"`, { + archiverRestartPromises.push(util.pm2Restart(networkDir, `"archive-server-${port + 1}"`, { ARCHIVER_PORT: existingArchivers[i].port, ARCHIVER_PUBLIC_KEY: existingArchivers[i].publicKey, ARCHIVER_SECRET_KEY: archiverKeys[port].secretKey, ARCHIVER_INFO: activeArchiversEnv, ARCHIVER_DB: `archiver-db-${existingArchivers[i].port}`, - }); + })) port++; } + await Promise.all(archiverRestartPromises) } activeArchivers = [...activeArchivers, ...newArchiversInfo] activeArchivers.sort((a, b) => a.port - b.port) @@ -147,13 +151,16 @@ module.exports = async function (networkDir, options, args) { if (networkConfig.stoppedConsensors) stoppedConsensors = networkConfig.stoppedConsensors; else stoppedConsensors = []; + const consensorRestartPromises = [] for (let i = 0; i < restartConsensorCount; i++) { - await util.pm2Restart(networkDir, `"shardus-instance-${lowestPort}"`); + consensorRestartPromises.push(util.pm2Restart(networkDir, `"shardus-instance-${lowestPort}"`)) networkConfig.runningPorts.push(lowestPort); } + await Promise.all(consensorRestartPromises) + const portRestartPromises = [] networkConfig.runningPorts.forEach((port) => { if (num > 0) { - util.pm2Restart(networkDir, `"shardus-instance-${port}"`); + portRestartPromises.push(util.pm2Restart(networkDir, `"shardus-instance-${port}"`)) networkConfig.runningPorts = networkConfig.runningPorts.filter( (p) => p !== port ); @@ -161,6 +168,10 @@ module.exports = async function (networkDir, options, args) { } num--; }); + await Promise.all(portRestartPromises) + if (portRestartPromises.length > 0) { + console.log(`✓ Restarted ${portRestartPromises.length} node(s)`) + } networkConfig.stoppedConsensors = stoppedConsensors; } shell.ShellString(JSON.stringify(networkConfig, null, 2)).to(`network-config.json`) diff --git a/src/lib/start.js b/src/lib/start.js index 592dfe8..369381a 100644 --- a/src/lib/start.js +++ b/src/lib/start.js @@ -4,7 +4,20 @@ const path = require('path') const util = require('./util') const archiverKeys = require('../configs/archiver-config') -module.exports = async function (networkDir, num, type, pm2Args, options) { +module.exports = async function (networkDir, num, type, pm2Args, options = {}) { + // Resolve archiver and monitor paths BEFORE changing directory + const projectRoot = path.dirname(networkDir) + const archiverPath = require.resolve('@shardus/archiver', { paths: [projectRoot] }) + const monitorPath = require.resolve('@shardus/monitor-server', { paths: [projectRoot] }) + + // Resolve explorer path (may not exist in all projects) + let explorerPath + try { + explorerPath = require.resolve('explorer-server', { paths: [projectRoot] }) + } catch (e) { + // Explorer not installed, that's ok + } + shell.cd(networkDir) const instancesPath = path.join(process.cwd()) const configPath = path.join(instancesPath, 'network-config.json') @@ -28,22 +41,26 @@ module.exports = async function (networkDir, num, type, pm2Args, options) { if (newArchiverCount > 9) newArchiverCount = 9 const existingArchiversEnv = existingArchivers.map((archiver) => `${archiver.ip}:${archiver.port}:${archiver.publicKey}`).join(',') - // Start new archivers on ports following existingArchivers + // Start new archivers on ports following existingArchivers (parallel) + const archiverStarts = [] for (let i = 0; i < newArchiverCount; i++) { - await util.pm2Start( - networkDir, - require.resolve('@shardus/archiver', { paths: [process.cwd()] }), - `archive-server-${i + 1 + existingArchivers.length}`, - { - ARCHIVER_PORT: existingArchivers[0].port + existingArchivers.length + i, - ARCHIVER_PUBLIC_KEY: archiverKeys[existingArchivers.length + i].publicKey, - ARCHIVER_SECRET_KEY: archiverKeys[existingArchivers.length + i].secretKey, - ARCHIVER_INFO: existingArchiversEnv, - ARCHIVER_DB: `archiver-db-${archiverKeys[existingArchivers.length + i].port}` - }, - pm2Args + archiverStarts.push( + util.pm2Start( + networkDir, + archiverPath, + `archive-server-${i + 1 + existingArchivers.length}`, + { + ARCHIVER_PORT: existingArchivers[0].port + existingArchivers.length + i, + ARCHIVER_PUBLIC_KEY: archiverKeys[existingArchivers.length + i].publicKey, + ARCHIVER_SECRET_KEY: archiverKeys[existingArchivers.length + i].secretKey, + ARCHIVER_INFO: existingArchiversEnv, + ARCHIVER_DB: `archiver-db-${archiverKeys[existingArchivers.length + i].port}` + }, + pm2Args + ) ) } + await Promise.all(archiverStarts) // Add the newly started archivers to network-config.json existingArchivers for (let i = 1; i <= newArchiverCount; i++) { @@ -55,13 +72,13 @@ module.exports = async function (networkDir, num, type, pm2Args, options) { return } - // Start archiver + // Start archiver first (monitor needs it ready) if (networkConfig.startArchiver) { const existingArchivers = JSON.parse(networkConfig.existingArchivers) await util.pm2Start( networkDir, - require.resolve('@shardus/archiver', { paths: [process.cwd()] }), + archiverPath, `archive-server-1`, { ARCHIVER_PORT: existingArchivers[0].port, @@ -74,17 +91,18 @@ module.exports = async function (networkDir, num, type, pm2Args, options) { ) networkConfig.startArchiver = false // Prevent this code from running twice - - await util.sleep(1000) // Add 1sec delay to allow archiver to be ready, so that monitor can connect it with archiver discovery + + // Small delay to allow archiver to be ready for monitor + await util.sleep(1000) } - // Start monitor + // Start monitor (needs archiver to be ready) if (networkConfig.startMonitor) { let existingArchivers = JSON.parse(networkConfig.existingArchivers) const existingArchiversEnv = existingArchivers.map((archiver) => `${archiver.ip}:${archiver.port}:${archiver.publicKey}`).join(',') await util.pm2Start( networkDir, - require.resolve("@shardus/monitor-server", { paths: [process.cwd()] }), + monitorPath, "monitor-server", { PORT: new URL(networkConfig.monitorUrl).port, @@ -98,11 +116,11 @@ module.exports = async function (networkDir, num, type, pm2Args, options) { networkConfig.startMonitor = false; // Prevent this code from running twice } - // Start explorer - if (networkConfig.startExplorerServer) { + // Start explorer (can be parallel with nodes) + if (networkConfig.startExplorerServer && explorerPath) { await util.pm2Start( networkDir, - require.resolve('explorer-server', { paths: [process.cwd()] }), + explorerPath, 'explorer-server', { PORT: networkConfig.explorerServerPort }, pm2Args @@ -117,14 +135,20 @@ module.exports = async function (networkDir, num, type, pm2Args, options) { console.log(err) } + // Start all validator nodes in parallel (don't wait - fire and forget for speed) if (type === 'create') { + console.log(`Starting ${nodesToStart} validator nodes in parallel...`) for (let i = 0; i < nodesToStart; i++) { if (!networkConfig.runningPorts.includes(networkConfig.lowestPort + i)) { if (instances[i]) { if (options?.inspect) { - await util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, [`pm2--node-args="--inspect=127.0.0.1:${networkConfig.inspectPort + i}"`, ...pm2Args]) + util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, [`pm2--node-args="--inspect=127.0.0.1:${networkConfig.inspectPort + i}"`, ...pm2Args]).catch(err => { + console.error(`Error starting ${path.basename(instances[i])}:`, err.message) + }) } else { - await util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, pm2Args) + util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, pm2Args).catch(err => { + console.error(`Error starting ${path.basename(instances[i])}:`, err.message) + }) } networkConfig.runningPorts.push(networkConfig.lowestPort + i) } @@ -135,16 +159,23 @@ module.exports = async function (networkDir, num, type, pm2Args, options) { } if (type === 'start') { + console.log(`Starting ${num} validator nodes in parallel...`) for (let i = instances.length - num; i < instances.length; i++) { if (options?.inspect) { - await util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, [`pm2--node-args="--inspect=127.0.0.1:${networkConfig.inspectPort + i}"`, ...pm2Args]) + util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, [`pm2--node-args="--inspect=127.0.0.1:${networkConfig.inspectPort + i}"`, ...pm2Args]).catch(err => { + console.error(`Error starting ${path.basename(instances[i])}:`, err.message) + }) } else { - await util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, pm2Args) + util.pm2Start(networkDir, networkConfig.serverPath, path.basename(instances[i]), { BASE_DIR: instances[i] }, pm2Args).catch(err => { + console.error(`Error starting ${path.basename(instances[i])}:`, err.message) + }) } let port = parseInt(instances[i].split('-').pop()) networkConfig.runningPorts.push(port) } } + + console.log(`✓ Validator nodes starting (check with 'shardus pm2 list')`) shell.ShellString(JSON.stringify(networkConfig, null, 2)).to(`network-config.json`) diff --git a/src/lib/stop.js b/src/lib/stop.js index 54d0164..a6f05dc 100644 --- a/src/lib/stop.js +++ b/src/lib/stop.js @@ -22,15 +22,19 @@ module.exports = async function (networkDir, num, options) { if (existingArchivers.length > stopArchiverCount) { const activeArchivers = existingArchivers.slice(stopArchiverCount, existingArchivers.length) - // Stop archivers on ports following existingArchivers + // Stop archivers on ports following existingArchivers (parallel) + const archiverStops = [] for (let i = 0; i < stopArchiverCount; i++) { - await util.pm2Stop( - networkDir, - `"archive-server-${stoppedArchiversCount + 1}"` + archiverStops.push( + util.pm2Stop( + networkDir, + `"archive-server-${stoppedArchiversCount + 1}"` + ) ) stoppedArchivers.push(existingArchivers[i]) stoppedArchiversCount++ } + await Promise.all(archiverStops) activeArchivers.sort((a, b) => a.port - b.port) networkConfig.existingArchivers = JSON.stringify(activeArchivers) networkConfig.stoppedArchivers = JSON.stringify(stoppedArchivers) @@ -73,17 +77,24 @@ module.exports = async function (networkDir, num, options) { stoppedConsensors = networkConfig.stoppedConsensors else stoppedConsensors = [] + + // Stop nodes in parallel for speed + const nodeStops = [] networkConfig.runningPorts.forEach(port => { if (num > 0) { - util.pm2Stop(networkDir, `"shardus-instance-${port}"`) + nodeStops.push(util.pm2Stop(networkDir, `"shardus-instance-${port}"`)) networkConfig.runningPorts = networkConfig.runningPorts.filter(p => p !== port) stoppedConsensors.push(port) } num-- }) + await Promise.all(nodeStops) + console.log(`✓ Stopped ${nodeStops.length} node(s)`) networkConfig.stoppedConsensors = stoppedConsensors } else { - util.pm2Stop(networkDir, 'all') + console.log('Stopping all processes...') + await util.pm2Stop(networkDir, 'all') + console.log('✓ All processes stopped') networkConfig.runningPorts = [] networkConfig.startArchiver = true networkConfig.startMonitor = true diff --git a/src/lib/util.js b/src/lib/util.js index a4fde50..9bf17a7 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -10,7 +10,8 @@ const pm2Start = async (networkDir, script, name, env = {}, pm2Args = []) => { const parsedPm2Args = pm2Args.map((arg) => arg.split('pm2')[1] || arg).join(' ') const execaCmd = `${pm2} start ${script} --name="${name}" ${parsedPm2Args}` console.log('pm2Start', execaCmd) - await execa.command(execaCmd, { cwd: networkDir, env, stdio: [0, 1, 2] }) + // Suppress output for cleaner logs, but still awaitable + await execa.command(execaCmd, { cwd: networkDir, env, stdio: 'ignore' }) } const pm2Restart = async (networkDir, name, env = {}) => { @@ -18,61 +19,175 @@ const pm2Restart = async (networkDir, name, env = {}) => { // const parsedPm2Args = pm2Args.map((arg) => arg.split('pm2')[1] || arg).join(' ') const execaCmd = `${pm2} restart ${name} --update-env` console.log('pm2Restart', execaCmd, env) - await execa.command(execaCmd, { cwd: networkDir, env, stdio: [0, 1, 2] }) + // Fire and forget - don't await, suppress output for speed + execa.command(execaCmd, { cwd: networkDir, env, stdio: 'ignore' }).catch(err => { + console.error(`Error restarting ${name}:`, err.message) + }) } const pm2Stop = async (networkDir, arg, env = {}) => { env.PM2_HOME = path.join(networkDir, '.pm2/') - await execa.command(`${pm2} stop ${arg}`, { + // Don't await - fire and forget for speed, suppress output + execa.command(`${pm2} stop ${arg}`, { cwd: networkDir, env, - stdio: [0, 1, 2], + stdio: 'ignore', + }).catch(err => { + console.error(`Error stopping ${arg}:`, err.message) }) } const pm2Kill = async (networkDir, env = {}) => { env.PM2_HOME = path.join(networkDir, '.pm2/') - await execa.command(`${pm2} kill`, { cwd: networkDir, env, stdio: [0, 1, 2] }) + await execa.command(`${pm2} kill`, { cwd: networkDir, env, stdio: 'ignore' }) } const pm2Reset = async (arg, env = {}) => { env.PM2_HOME = path.join(networkDir, '.pm2/') - await execa.command(`${pm2} reset ${arg}`, { env, stdio: [0, 1, 2] }) + await execa.command(`${pm2} reset ${arg}`, { env, stdio: 'ignore' }) } const pm2Del = async (arg, env = {}) => { env.PM2_HOME = path.join(networkDir, '.pm2/') - await execa.command(`${pm2} del ${arg}`, { env, stdio: [0, 1, 2] }) + await execa.command(`${pm2} del ${arg}`, { env, stdio: 'ignore' }) } const pm2List = async (networkDir, env = {}) => { env.PM2_HOME = path.join(networkDir, '.pm2/') - await execa.command(`${pm2} list`, { cwd: networkDir, env, stdio: [0, 1, 2] }) + await execa.command(`${pm2} list`, { cwd: networkDir, env, stdio: 'inherit' }) +} + +const pm2CheckModuleInstalled = async (networkDir, moduleName, env = {}) => { + env.PM2_HOME = path.join(networkDir, '.pm2/') + try { + const result = await execa.command(`${pm2} list`, { cwd: networkDir, env }) + return result.stdout.includes(moduleName) + } catch (err) { + return false + } +} + +const pm2GetModuleConfig = async (networkDir, env = {}) => { + env.PM2_HOME = path.join(networkDir, '.pm2/') + try { + const moduleConfPath = path.join(networkDir, '.pm2', 'module_conf.json') + if (fs.existsSync(moduleConfPath)) { + return JSON.parse(fs.readFileSync(moduleConfPath, 'utf-8')) + } + return {} + } catch (err) { + return {} + } } const pm2InstallRotateLog = async (networkDir, env = {}) => { env.PM2_HOME = path.join(networkDir, '.pm2/') + + // Check if pm2-logrotate is already installed + const isInstalled = await pm2CheckModuleInstalled(networkDir, 'pm2-logrotate', env) + + if (isInstalled) { + console.log('pm2-logrotate already installed') + return + } + + console.log('Installing pm2-logrotate...') + + // Install with output suppressed to avoid verbose restarts logs await execa.command(`${pm2} install pm2-logrotate`, { cwd: networkDir, env, - stdio: [0, 1, 2], + stdio: 'ignore', }) + + console.log('✓ pm2-logrotate installed') } const pm2SetRotateLog = async (networkDir, maxSizeMb = 10, retain = 10, env = {}) => { env.PM2_HOME = path.join(networkDir, '.pm2/') - await execa.command(`${pm2} set pm2-logrotate:max_size ${maxSizeMb}M`, { - cwd: networkDir, - env, - stdio: [0, 1, 2], - }) - await execa.command(`${pm2} set pm2-logrotate:retain ${retain}`, { - cwd: networkDir, - env, - stdio: [0, 1, 2], - }) - // Displays PM2s conf variables - await execa.command(`${pm2} conf`, { cwd: networkDir, env, stdio: [0, 1, 2] }) + + // Check current configuration to avoid unnecessary restarts + const currentConfig = await pm2GetModuleConfig(networkDir, env) + const needsUpdate = + !currentConfig['pm2-logrotate'] || + currentConfig['pm2-logrotate']['max_size'] !== `${maxSizeMb}M` || + currentConfig['pm2-logrotate']['retain'] !== retain + + if (!needsUpdate) { + console.log(`pm2-logrotate already configured (max_size: ${maxSizeMb}M, retain: ${retain})`) + return + } + + console.log(`Configuring pm2-logrotate (max_size: ${maxSizeMb}M, retain: ${retain})...`) + + // Only set if configuration is different - suppress output to avoid noise + if (!currentConfig['pm2-logrotate'] || currentConfig['pm2-logrotate']['max_size'] !== `${maxSizeMb}M`) { + await execa.command(`${pm2} set pm2-logrotate:max_size ${maxSizeMb}M`, { + cwd: networkDir, + env, + stdio: 'ignore', + }) + } + + if (!currentConfig['pm2-logrotate'] || currentConfig['pm2-logrotate']['retain'] !== retain) { + await execa.command(`${pm2} set pm2-logrotate:retain ${retain}`, { + cwd: networkDir, + env, + stdio: 'ignore', + }) + } + + console.log('✓ pm2-logrotate configured') +} + +const pm2SetupLogRotation = async (networkDir, maxSizeMb = 10, retain = 10, env = {}) => { + env.PM2_HOME = path.join(networkDir, '.pm2/') + + // Check if pm2-logrotate is already installed and configured + const isInstalled = await pm2CheckModuleInstalled(networkDir, 'pm2-logrotate', env) + const currentConfig = await pm2GetModuleConfig(networkDir, env) + + const needsConfigUpdate = + !currentConfig['pm2-logrotate'] || + currentConfig['pm2-logrotate']['max_size'] !== `${maxSizeMb}M` || + currentConfig['pm2-logrotate']['retain'] !== retain + + // If already installed and configured correctly, skip everything + if (isInstalled && !needsConfigUpdate) { + console.log(`✓ pm2-logrotate already configured (max_size: ${maxSizeMb}M, retain: ${retain})`) + return + } + + // Install if not installed (with suppressed output) + if (!isInstalled) { + console.log('Setting up pm2-logrotate...') + await execa.command(`${pm2} install pm2-logrotate`, { + cwd: networkDir, + env, + stdio: 'ignore', + }) + } + + // Configure if needed (suppress output to avoid restart noise) + if (needsConfigUpdate) { + if (!currentConfig['pm2-logrotate'] || currentConfig['pm2-logrotate']['max_size'] !== `${maxSizeMb}M`) { + await execa.command(`${pm2} set pm2-logrotate:max_size ${maxSizeMb}M`, { + cwd: networkDir, + env, + stdio: 'ignore', + }) + } + + if (!currentConfig['pm2-logrotate'] || currentConfig['pm2-logrotate']['retain'] !== retain) { + await execa.command(`${pm2} set pm2-logrotate:retain ${retain}`, { + cwd: networkDir, + env, + stdio: 'ignore', + }) + } + } + + console.log(`✓ pm2-logrotate configured (max_size: ${maxSizeMb}M, retain: ${retain})`) } const pm2Exec = async (networkDir, arg, env = {}) => { @@ -159,8 +274,9 @@ module.exports = { pm2List, pm2InstallRotateLog, pm2SetRotateLog, + pm2SetupLogRotation, pm2Exec, checkNetworkFolder, setNetworkDirOrErr, sleep -} +} \ No newline at end of file