tcp-serial-relay
Version:
Modular TCP to Serial relay service with comprehensive logging, monitoring and web dashboard
403 lines (332 loc) • 11.9 kB
JavaScript
// 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;