UNPKG

tcp-serial-relay

Version:

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

501 lines (444 loc) 16 kB
#!/usr/bin/env node // scripts/config-manager.js - Configuration management utility const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const { program } = require('commander'); // Import the config path resolver const { getConfigPath } = require('../src/config'); class ConfigManager { constructor() { this.configPath = getConfigPath(); // Use the same path resolution as the main app this.defaultConfigPath = path.join(__dirname, '..', 'src', 'config', 'default-config.js'); } show() { console.log('Current Configuration:'); console.log('===================='); console.log(`Configuration file: ${this.configPath}`); console.log(''); if (!fs.existsSync(this.configPath)) { console.log('❌ Configuration file not found'); console.log('Run "tcp-serial-relay config --reset" to create default configuration'); return; } try { const config = JSON.parse(fs.readFileSync(this.configPath, 'utf8')); console.log(JSON.stringify(config, null, 2)); } catch (error) { console.error('❌ Error reading configuration:', error.message); } } edit() { if (!fs.existsSync(this.configPath)) { console.log('Configuration file not found. Creating default configuration...'); this.reset(); } const editor = process.env.EDITOR || 'nano'; console.log(`Opening configuration file with ${editor}...`); console.log(`File: ${this.configPath}`); const child = spawn(editor, [this.configPath], { stdio: 'inherit' }); child.on('close', (code) => { if (code === 0) { console.log('Configuration file saved. Validating...'); this.validate(); } else { console.log('Editor exited with code:', code); } }); } validate(configFile = null) { const targetFile = configFile || this.configPath; console.log('Validating configuration...'); console.log('File:', targetFile); if (!fs.existsSync(targetFile)) { console.log('❌ Configuration file not found'); return false; } try { const config = JSON.parse(fs.readFileSync(targetFile, 'utf8')); // Validation rules const validations = [ { field: 'tcpIp', test: (val) => typeof val === 'string' && val.length > 0, message: 'TCP IP must be a non-empty string' }, { field: 'tcpPort', test: (val) => Number.isInteger(val) && val >= 1 && val <= 65535, message: 'TCP port must be an integer between 1 and 65535' }, { field: 'connectionType', test: (val) => ['serial', 'tcp'].includes(val), message: 'Connection type must be "serial" or "tcp"' } ]; // Connection-specific validations if (config.connectionType === 'serial') { validations.push( { field: 'serialPath', test: (val) => typeof val === 'string' && val.length > 0, message: 'Serial path must be a non-empty string' }, { field: 'serialBaud', test: (val) => Number.isInteger(val) && val > 0, message: 'Serial baud rate must be a positive integer' }, { field: 'serialParity', test: (val) => ['none', 'even', 'odd', 'mark', 'space'].includes(val), message: 'Serial parity must be one of: none, even, odd, mark, space' }, { field: 'serialDataBits', test: (val) => [5, 6, 7, 8].includes(val), message: 'Serial data bits must be 5, 6, 7, or 8' }, { field: 'serialStopBits', test: (val) => [1, 1.5, 2].includes(val), message: 'Serial stop bits must be 1, 1.5, or 2' } ); } else if (config.connectionType === 'tcp') { validations.push( { field: 'secondaryTcpIp', test: (val) => typeof val === 'string' && val.length > 0, message: 'Secondary TCP IP must be a non-empty string' }, { field: 'secondaryTcpPort', test: (val) => Number.isInteger(val) && val >= 1 && val <= 65535, message: 'Secondary TCP port must be an integer between 1 and 65535' } ); } let isValid = true; const errors = []; for (const validation of validations) { if (!config.hasOwnProperty(validation.field)) { errors.push(`Missing required field: ${validation.field}`); isValid = false; } else if (!validation.test(config[validation.field])) { errors.push(`${validation.field}: ${validation.message}`); isValid = false; } } // Check for port conflicts in TCP mode if (config.connectionType === 'tcp' && config.tcpIp === config.secondaryTcpIp && config.tcpPort === config.secondaryTcpPort) { errors.push('Primary and secondary TCP endpoints cannot be the same'); isValid = false; } if (isValid) { console.log('✅ Configuration is valid'); console.log('\nConfiguration Summary:'); console.log(` Connection Type: ${config.connectionType}`); console.log(` Primary TCP: ${config.tcpIp}:${config.tcpPort}`); if (config.connectionType === 'serial') { console.log(` Serial Port: ${config.serialPath} @ ${config.serialBaud} baud`); console.log(` Serial Settings: ${config.serialDataBits}${config.serialParity.charAt(0).toUpperCase()}${config.serialStopBits}`); } else if (config.connectionType === 'tcp') { console.log(` Secondary TCP: ${config.secondaryTcpIp}:${config.secondaryTcpPort}`); } console.log(` Log Level: ${config.logLevel || 'info'}`); return true; } else { console.log('❌ Configuration validation failed:'); errors.forEach(error => console.log(` - ${error}`)); return false; } } catch (error) { console.log('❌ Configuration parsing failed:', error.message); return false; } } reset() { console.log('Resetting configuration to defaults...'); console.log(`Target file: ${this.configPath}`); try { // Load default configuration delete require.cache[require.resolve(this.defaultConfigPath)]; const defaultConfig = require(this.defaultConfigPath); // Ensure config directory exists const configDir = path.dirname(this.configPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); console.log(`Created directory: ${configDir}`); } // Write default configuration fs.writeFileSync(this.configPath, JSON.stringify(defaultConfig, null, 2), 'utf8'); console.log('✅ Default configuration created'); console.log('\nNext steps:'); console.log('1. Edit the configuration: tcp-serial-relay config --edit'); console.log('2. Validate: tcp-serial-relay config --validate'); console.log('3. Test connections: tcp-serial-relay test'); } catch (error) { console.error('❌ Failed to reset configuration:', error.message); } } backup() { if (!fs.existsSync(this.configPath)) { console.log('❌ No configuration file to backup'); return; } const backupDir = path.join(path.dirname(this.configPath), 'backups'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupFile = path.join(backupDir, `relay-config-${timestamp}.json`); try { // Ensure backup directory exists if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } // Copy configuration file fs.copyFileSync(this.configPath, backupFile); console.log('✅ Configuration backed up'); console.log('Backup file:', backupFile); } catch (error) { console.error('❌ Failed to backup configuration:', error.message); } } restore(backupFile) { if (!fs.existsSync(backupFile)) { console.log('❌ Backup file not found:', backupFile); // List available backups const backupDir = path.join(path.dirname(this.configPath), 'backups'); if (fs.existsSync(backupDir)) { const backups = fs.readdirSync(backupDir) .filter(file => file.endsWith('.json')) .sort() .reverse(); if (backups.length > 0) { console.log('\nAvailable backups:'); backups.forEach(backup => { const backupPath = path.join(backupDir, backup); const stats = fs.statSync(backupPath); console.log(` ${backup} (${stats.mtime.toISOString()})`); }); } else { console.log('No backup files found'); } } return; } try { // Validate backup file first if (!this.validate(backupFile)) { console.log('❌ Backup file is invalid, cannot restore'); return; } // Backup current config before restoring this.backup(); // Restore from backup fs.copyFileSync(backupFile, this.configPath); console.log('✅ Configuration restored from backup'); console.log('Restored from:', backupFile); console.log('Current config:', this.configPath); } catch (error) { console.error('❌ Failed to restore configuration:', error.message); } } info() { console.log('Configuration Information:'); console.log('========================='); console.log(`Config path: ${this.configPath}`); console.log(`Config exists: ${fs.existsSync(this.configPath) ? 'Yes' : 'No'}`); if (fs.existsSync(this.configPath)) { const stats = fs.statSync(this.configPath); console.log(`Last modified: ${stats.mtime.toISOString()}`); console.log(`File size: ${stats.size} bytes`); } console.log(`Default config: ${this.defaultConfigPath}`); console.log(''); // Show environment variables that affect config const envVars = [ 'CONFIG_PATH', 'TCP_IP', 'TCP_PORT', 'CONNECTION_TYPE', 'SERIAL_PATH', 'SERIAL_BAUD', 'SECONDARY_TCP_IP', 'SECONDARY_TCP_PORT' ]; const setVars = envVars.filter(name => process.env[name]); if (setVars.length > 0) { console.log('Environment overrides:'); setVars.forEach(name => { console.log(` ${name}=${process.env[name]}`); }); } else { console.log('No environment variable overrides set'); } } template() { const template = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "TCP-Serial/TCP Relay Configuration", "type": "object", "properties": { "connectionType": { "type": "string", "enum": ["serial", "tcp"], "description": "Type of secondary connection", "default": "serial" }, "tcpIp": { "type": "string", "description": "Primary TCP server IP address", "default": "192.168.1.90" }, "tcpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "Primary TCP server port", "default": 10002 }, "serialPath": { "type": "string", "description": "Serial port device path (when connectionType is 'serial')", "default": "/dev/ttyUSB0" }, "serialBaud": { "type": "integer", "minimum": 1, "description": "Serial port baud rate (when connectionType is 'serial')", "default": 9600 }, "serialParity": { "type": "string", "enum": ["none", "even", "odd", "mark", "space"], "description": "Serial port parity (when connectionType is 'serial')", "default": "odd" }, "serialDataBits": { "type": "integer", "enum": [5, 6, 7, 8], "description": "Serial port data bits (when connectionType is 'serial')", "default": 7 }, "serialStopBits": { "type": "number", "enum": [1, 1.5, 2], "description": "Serial port stop bits (when connectionType is 'serial')", "default": 1 }, "secondaryTcpIp": { "type": "string", "description": "Secondary TCP server IP address (when connectionType is 'tcp')", "default": "192.168.1.91" }, "secondaryTcpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "Secondary TCP server port (when connectionType is 'tcp')", "default": 10003 }, "maxRetries": { "type": "integer", "minimum": 0, "description": "Maximum connection retry attempts", "default": 3 }, "retryDelay": { "type": "integer", "minimum": 100, "description": "Delay between retry attempts (ms)", "default": 5000 }, "connectionTimeout": { "type": "integer", "minimum": 1000, "description": "Connection timeout (ms)", "default": 10000 }, "relayTimeout": { "type": "integer", "minimum": 1000, "description": "Maximum time to wait for data (ms)", "default": 30000 }, "logDataTransfers": { "type": "boolean", "description": "Log individual data transfers", "default": true }, "logLevel": { "type": "string", "enum": ["error", "warn", "info", "debug", "silly"], "description": "Logging level", "default": "info" } }, "required": [ "connectionType", "tcpIp", "tcpPort" ], "if": { "properties": { "connectionType": { "const": "serial" } } }, "then": { "required": ["serialPath", "serialBaud", "serialParity", "serialDataBits", "serialStopBits"] }, "else": { "required": ["secondaryTcpIp", "secondaryTcpPort"] } }; console.log('Configuration Template:'); console.log('======================'); console.log(JSON.stringify(template, null, 2)); } } // CLI setup program .name('config-manager') .description('TCP-Serial/TCP Relay configuration management') .version('1.0.0'); program .option('--show', 'Show current configuration') .option('--edit', 'Edit configuration file') .option('--validate [file]', 'Validate configuration') .option('--reset', 'Reset to default configuration') .option('--backup', 'Backup current configuration') .option('--restore <file>', 'Restore from backup file') .option('--info', 'Show configuration file information') .option('--template', 'Show configuration template/schema') .action((options) => { const manager = new ConfigManager(); if (options.show) { manager.show(); } else if (options.edit) { manager.edit(); } else if (options.validate !== undefined) { const file = typeof options.validate === 'string' ? options.validate : null; manager.validate(file); } else if (options.reset) { manager.reset(); } else if (options.backup) { manager.backup(); } else if (options.restore) { manager.restore(options.restore); } else if (options.info) { manager.info(); } else if (options.template) { manager.template(); } else { program.help(); } }); program.parse(process.argv); // Show help if no options provided if (process.argv.length <= 2) { program.help(); } module.exports = ConfigManager;