UNPKG

tcp-serial-relay

Version:

Modular TCP to Serial relay service with comprehensive logging, monitoring and web dashboard

403 lines (332 loc) 11.9 kB
#!/usr/bin/env node // scripts/install-service.js - Install system service (cron or systemd) const fs = require('fs'); const path = require('path'); const os = require('os'); const { exec } = require('child_process'); const { promisify } = require('util'); const { program } = require('commander'); const execAsync = promisify(exec); class ServiceInstaller { constructor(options = {}) { this.user = options.user || 'relay'; this.packageRoot = this.findPackageRoot(); this.binPath = this.findBinaryPath(); this.configPath = '/etc/tcp-serial-relay/relay-config.json'; this.logDir = '/var/log/tcp-serial-relay'; this.statusDir = '/opt/tcp-serial-relay/status'; } findPackageRoot() { let dir = __dirname; while (dir !== path.dirname(dir)) { if (fs.existsSync(path.join(dir, 'package.json'))) { try { const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); if (pkg.name === '@yourcompany/tcp-serial-relay') { return dir; } } catch (error) { // Continue searching } } dir = path.dirname(dir); } return __dirname; } findBinaryPath() { // Try common locations for global npm installs const possiblePaths = [ '/usr/local/bin/tcp-serial-relay', '/usr/bin/tcp-serial-relay', path.join(process.execPath, '..', 'tcp-serial-relay'), 'tcp-serial-relay' // Fallback to PATH ]; for (const binPath of possiblePaths) { if (fs.existsSync(binPath)) { return binPath; } } return 'tcp-serial-relay'; // Use PATH } async installCronService() { console.log('Installing TCP-Serial Relay as cron service...'); console.log(`User: ${this.user}`); try { // Check if user exists await execAsync(`id ${this.user}`); } catch (error) { console.log(`Creating user: ${this.user}`); try { await execAsync(`sudo useradd -r -s /bin/false -d /opt/tcp-serial-relay ${this.user}`); await execAsync(`sudo usermod -a -G dialout ${this.user}`); } catch (createError) { console.error('Failed to create user:', createError.message); return false; } } // Create cron script const cronScript = this.generateCronScript(); const cronScriptPath = '/opt/tcp-serial-relay/scripts/tcp-serial-relay-cron.sh'; try { // Ensure directories exist await execAsync('sudo mkdir -p /opt/tcp-serial-relay/scripts'); await execAsync(`sudo mkdir -p ${this.logDir}`); await execAsync(`sudo mkdir -p ${this.statusDir}`); // Write cron script const tempScript = path.join(os.tmpdir(), 'tcp-serial-relay-cron.sh'); fs.writeFileSync(tempScript, cronScript); await execAsync(`sudo mv ${tempScript} ${cronScriptPath}`); await execAsync(`sudo chmod +x ${cronScriptPath}`); await execAsync(`sudo chown ${this.user}:${this.user} ${cronScriptPath}`); // Set up permissions await execAsync(`sudo chown -R ${this.user}:${this.user} ${this.logDir}`); await execAsync(`sudo chown -R ${this.user}:${this.user} ${this.statusDir}`); // Install crontab const cronEntry = `0 * * * * ${cronScriptPath}`; await execAsync(`echo "${cronEntry}" | sudo -u ${this.user} crontab -`); console.log('✅ Cron service installed successfully'); console.log(` Schedule: Runs every hour at minute 0`); console.log(` Script: ${cronScriptPath}`); console.log(` Logs: ${this.logDir}/cron.log`); return true; } catch (error) { console.error('❌ Failed to install cron service:', error.message); return false; } } generateCronScript() { return `#!/bin/bash # TCP-Serial Relay Cron Script # Generated by tcp-serial-relay install-service APP_DIR="/opt/tcp-serial-relay" LOG_DIR="${this.logDir}" CONFIG_PATH="${this.configPath}" STATUS_DIR="${this.statusDir}" LOCK_FILE="/tmp/tcp-serial-relay.lock" BIN_PATH="${this.binPath}" # Function to log with timestamp log_message() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DIR/cron.log" } # Check if already running if [[ -f "$LOCK_FILE" ]]; then pid=$(cat "$LOCK_FILE") if kill -0 "$pid" 2>/dev/null; then log_message "Relay already running (PID: $pid), skipping this run" exit 0 else log_message "Stale lock file found, removing..." rm -f "$LOCK_FILE" fi fi # Create lock file echo $$ > "$LOCK_FILE" # Ensure directories exist mkdir -p "$STATUS_DIR" mkdir -p "$LOG_DIR" # Generate unique run ID RUN_ID="cron-$(date +'%Y%m%d-%H%M%S')-$$" STATUS_FILE="$STATUS_DIR/status-$RUN_ID.json" log_message "Starting relay service (Run ID: $RUN_ID)" # Set environment export NODE_ENV=production export LOG_LEVEL=info export CONFIG_PATH="$CONFIG_PATH" # Run the relay service with timeout timeout 300 "$BIN_PATH" start --config "$CONFIG_PATH" 2>&1 | tee -a "$LOG_DIR/cron.log" EXIT_CODE=\${PIPESTATUS[0]} # Check results if [[ $EXIT_CODE -eq 0 ]]; then log_message "Relay service completed successfully (Run ID: $RUN_ID)" elif [[ $EXIT_CODE -eq 124 ]]; then log_message "Relay service timed out after 5 minutes (Run ID: $RUN_ID)" else log_message "Relay service failed with exit code $EXIT_CODE (Run ID: $RUN_ID)" fi # Clean up old status files (keep last 24) find "$STATUS_DIR" -name "status-*.json" -mtime +1 -delete 2>/dev/null || true # Clean up old log files (keep last 7 days) find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null || true # Remove lock file rm -f "$LOCK_FILE" log_message "Cron job completed (Run ID: $RUN_ID, Exit Code: $EXIT_CODE)" exit $EXIT_CODE `; } async installSystemdService() { console.log('Installing TCP-Serial Relay as systemd service...'); try { // Create systemd service file const serviceContent = this.generateSystemdService(); const servicePath = '/etc/systemd/system/tcp-serial-relay.service'; // Write service file const tempService = path.join(os.tmpdir(), 'tcp-serial-relay.service'); fs.writeFileSync(tempService, serviceContent); await execAsync(`sudo mv ${tempService} ${servicePath}`); await execAsync('sudo systemctl daemon-reload'); console.log('✅ Systemd service installed successfully'); console.log(` Service file: ${servicePath}`); console.log(''); console.log('To enable and start the service:'); console.log(' sudo systemctl enable tcp-serial-relay'); console.log(' sudo systemctl start tcp-serial-relay'); console.log(''); console.log('To check status:'); console.log(' sudo systemctl status tcp-serial-relay'); return true; } catch (error) { console.error('❌ Failed to install systemd service:', error.message); return false; } } generateSystemdService() { return `[Unit] Description=TCP-Serial Relay Service Documentation=https://github.com/yourcompany/tcp-serial-relay After=network.target Wants=network.target [Service] Type=simple User=${this.user} Group=${this.user} WorkingDirectory=/opt/tcp-serial-relay # Environment Environment=NODE_ENV=production Environment=LOG_LEVEL=info Environment=CONFIG_PATH=${this.configPath} # Command ExecStart=${this.binPath} start --config ${this.configPath} ExecReload=/bin/kill -USR1 $MAINPID # Restart policy Restart=on-failure RestartSec=5 StartLimitInterval=60 StartLimitBurst=3 # Logging StandardOutput=journal StandardError=journal SyslogIdentifier=tcp-serial-relay # Security settings NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=${this.logDir} /opt/tcp-serial-relay/logs /opt/tcp-serial-relay/status # Limits LimitNOFILE=1024 MemoryMax=256M [Install] WantedBy=multi-user.target `; } async uninstallServices() { console.log('Uninstalling TCP-Serial Relay services...'); let uninstalledAny = false; // Remove cron job try { await execAsync(`sudo -u ${this.user} crontab -r`); console.log('✅ Cron job removed'); uninstalledAny = true; } catch (error) { console.log(' No cron job found for user:', this.user); } // Remove systemd service try { await execAsync('sudo systemctl stop tcp-serial-relay'); await execAsync('sudo systemctl disable tcp-serial-relay'); await execAsync('sudo rm -f /etc/systemd/system/tcp-serial-relay.service'); await execAsync('sudo systemctl daemon-reload'); console.log('✅ Systemd service removed'); uninstalledAny = true; } catch (error) { console.log(' No systemd service found'); } // Remove cron script try { await execAsync('sudo rm -f /opt/tcp-serial-relay/scripts/tcp-serial-relay-cron.sh'); console.log('✅ Cron script removed'); } catch (error) { // Ignore errors } if (uninstalledAny) { console.log('Services uninstalled successfully'); } else { console.log('No services were installed'); } return true; } async checkServiceStatus() { console.log('Checking service status...'); console.log(''); // Check cron job try { const { stdout } = await execAsync(`sudo -u ${this.user} crontab -l`); if (stdout.includes('tcp-serial-relay')) { console.log('✅ Cron service: Installed'); const cronLine = stdout.split('\n').find(line => line.includes('tcp-serial-relay')); console.log(` Schedule: ${cronLine}`); } else { console.log('❌ Cron service: Not installed'); } } catch (error) { console.log('❌ Cron service: Not installed'); } console.log(''); // Check systemd service try { const { stdout } = await execAsync('sudo systemctl is-active tcp-serial-relay'); const isActive = stdout.trim() === 'active'; if (isActive) { console.log('✅ Systemd service: Running'); } else { console.log('⚠️ Systemd service: Installed but not running'); } // Get more details try { const { stdout: statusOutput } = await execAsync('sudo systemctl status tcp-serial-relay --no-pager -l'); console.log(' Status details:'); console.log(statusOutput.split('\n').slice(0, 5).map(line => ` ${line}`).join('\n')); } catch (statusError) { // Ignore } } catch (error) { console.log('❌ Systemd service: Not installed'); } } } // CLI setup program .name('install-service') .description('Install TCP-Serial Relay as system service') .version('1.0.0'); program .option('--cron', 'Install as cron job') .option('--systemd', 'Install as systemd service') .option('--user <user>', 'Service user', 'relay') .option('--uninstall', 'Uninstall all services') .option('--status', 'Check service status') .action(async (options) => { const installer = new ServiceInstaller({ user: options.user }); if (options.uninstall) { await installer.uninstallServices(); } else if (options.status) { await installer.checkServiceStatus(); } else if (options.cron) { await installer.installCronService(); } else if (options.systemd) { await installer.installSystemdService(); } else { // Default: ask user which service type console.log('Choose installation type:'); console.log(' --cron Install as cron job (recommended for intermittent use)'); console.log(' --systemd Install as systemd service (for continuous operation)'); console.log(' --status Check current service status'); console.log(' --uninstall Remove all services'); } }); program.parse(process.argv); // Show help if no options provided if (process.argv.length <= 2) { program.help(); } module.exports = ServiceInstaller;