nelson-cloud
Version:
nelson on the cloud
258 lines (237 loc) • 9.22 kB
JavaScript
//=============================================================================
// Copyright (c) 2016-present Allan CORNET (Nelson)
//=============================================================================
// LICENCE_BLOCK_BEGIN
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// LICENCE_BLOCK_END
//=============================================================================
;
//=============================================================================
// Import required modules
const CONFIGURATION = require('../etc/configuration.json');
const { spawn } = require('child_process');
const { Address4, Address6 } = require('ip-address');
const socketio = require('socket.io');
const os = require('os');
//=============================================================================
// Start the Nelson process
startNelsonProcess();
//=============================================================================
// Define frozen object of socket event types to ensure consistency
const SOCKET_EVENTS = Object.freeze({
PROMPT: 'prompt', // Handling user prompts
CLC: 'clc', // Clear console event
QUIT: 'quit', // Quit application event
AVAILABLE: 'available', // Application availability status
UNAVAILABLE: 'unavailable',
DISCONNECT: 'disconnect', // Socket disconnection
SIOEMIT: 'sioemit', // Socket IO emit event
COMMAND_RECEIVED: 'command_received', // Command received acknowledgment
REPLY: 'reply', // Response to a command
});
//=============================================================================
/**
* Main function to start and manage the Nelson process
* Handles inter-process communication via Socket.IO
*/
function startNelsonProcess() {
let nelsonApp; // Child process running Nelson application
let commandio; // Socket.IO instance for command communication
let hasCleaned = false;
function cleanup(terminateNelson = false) {
if (hasCleaned) {
return;
}
hasCleaned = true;
if (terminateNelson) {
nelsonApp?.kill();
}
commandio?.close?.();
commandio = undefined;
nelsonApp = undefined;
}
process.once('SIGINT', () => cleanup(true));
process.once('SIGTERM', () => cleanup(true));
process.once('disconnect', () => cleanup(true));
process.once('exit', () => cleanup());
// Listen for messages from the parent process
process.on('message', (msg) => {
const handler = messageHandlers[msg.msgtype];
if (handler) {
handler(msg);
} else {
console.warn(`Unknown message: ${msg.msgtype}`);
}
});
//=============================================================================
/**
* Initialize socket connection and start Nelson application
* @param {number} port - Port number for socket connection
*/
function handleInitialization(port) {
hasCleaned = false;
// Set up socket connection
const { io: socketIOInstance, address: commandAddress } =
initializeSocket(port);
commandio = socketIOInstance;
// Bind socket events to process
commandio.on('connection', (socket) => {
bindSocketToProcess(socket, process);
});
// Determine and spawn Nelson application
const { app, parameters } = getAppAndParameters(commandAddress);
nelsonApp = spawn(app, parameters);
nelsonApp.on('exit', (code, signal) => {
process.send?.({ msgtype: 'process_exit', code, signal });
cleanup();
});
nelsonApp.on('error', (error) => {
process.send?.({ msgtype: 'process_error', error: error.message });
});
// Handle socket disconnection
commandio.on('disconnect', () => {
console.info('Socket disconnected');
});
}
//=============================================================================
/**
* Initialize Socket.IO with a given port and find local IPv4 address
* @param {number} port - Port number to use for socket connection
* @returns {Object} Socket.IO instance and command address
*/
function initializeSocket(port) {
// Create Socket.IO instance
const ioInstance = socketio(port);
const addressInfo = resolveNetworkAddress();
const addressString =
addressInfo.family === 'IPv6'
? `http://[${addressInfo.address.correctForm()}]:${port}`
: `http://${addressInfo.address.address}:${port}`;
return {
io: ioInstance,
address: addressString,
};
}
//=============================================================================
/**
* Determine application and its parameters based on configuration
* @param {string} commandAddress - Socket connection address
* @returns {Object} Application path and parameters
*/
function getAppAndParameters(commandAddress) {
// Choose between Docker or native application based on configuration
const app = CONFIGURATION.USE_DOCKER
? CONFIGURATION.DOCKER
: CONFIGURATION.NELSON_APPLICATION;
// Get corresponding parameters
const baseParameters = CONFIGURATION.USE_DOCKER
? CONFIGURATION.DOCKER_PARAMETERS
: CONFIGURATION.NELSON_APPLICATION_PARAMETERS;
const parameters = [...(baseParameters ?? []), commandAddress];
return { app, parameters };
}
//=============================================================================
/**
* Resolve network address for the socket connection
* Favors IPv4 over IPv6, and external over internal addresses
* @returns {Object} Resolved address and family (IPv4/IPv6)
*/
function resolveNetworkAddress() {
const interfaces = Object.values(os.networkInterfaces())
.flat()
.filter(Boolean);
const candidate =
interfaces.find(
(details) => details.family === 'IPv4' && !details.internal,
) ??
interfaces.find(
(details) => details.family === 'IPv6' && !details.internal,
) ??
interfaces.find((details) => details.family === 'IPv4') ??
interfaces.find((details) => details.family === 'IPv6');
if (!candidate || typeof candidate.address !== 'string') {
throw new Error('No network address available.');
}
const addressValue = candidate.address.includes('%')
? candidate.address.split('%')[0]
: candidate.address;
const address =
candidate.family === 'IPv6'
? new Address6(addressValue)
: new Address4(addressValue);
return { address, family: candidate.family };
}
//=============================================================================
function emitToCommandIO(event, payload) {
if (!commandio) {
return;
}
payload === undefined
? commandio.emit(event)
: commandio.emit(event, payload);
}
const messageHandlers = {
initialization: (msg) => handleInitialization(msg.id),
command: (msg) => emitToCommandIO('command', { data: msg.data }),
prompt: (msg) => {
console.log(`emit prompt ${msg.data}`);
emitToCommandIO('prompt', msg.data);
},
clc: () => emitToCommandIO('clc'),
available: () => emitToCommandIO('available'),
stop: () => {
emitToCommandIO('stop');
cleanup(true);
},
quit: () => cleanup(true),
};
}
//=============================================================================
/**
* Bind socket events to the main process
* Enables communication between socket and parent process
* @param {Socket} socket - Socket.IO socket instance
* @param {Process} process - Parent process
*/
function bindSocketToProcess(socket, process) {
// Map socket events to process messages
socket.on(SOCKET_EVENTS.COMMAND_RECEIVED, () => {
process.send({ msgtype: SOCKET_EVENTS.COMMAND_RECEIVED });
});
socket.on(SOCKET_EVENTS.REPLY, (data) => {
process.send({ msgtype: SOCKET_EVENTS.REPLY, output: data.output });
});
socket.on(SOCKET_EVENTS.PROMPT, (data) => {
process.send({ msgtype: SOCKET_EVENTS.PROMPT, output: data });
});
socket.on(SOCKET_EVENTS.CLC, () => {
process.send({ msgtype: SOCKET_EVENTS.CLC });
});
socket.on(SOCKET_EVENTS.QUIT, () => {
process.send({ msgtype: SOCKET_EVENTS.QUIT });
});
socket.on(SOCKET_EVENTS.AVAILABLE, () => {
process.send({ msgtype: SOCKET_EVENTS.AVAILABLE });
});
socket.on(SOCKET_EVENTS.UNAVAILABLE, () => {
process.send({ msgtype: SOCKET_EVENTS.UNAVAILABLE });
});
socket.on(SOCKET_EVENTS.DISCONNECT, () => {
process.send({ msgtype: SOCKET_EVENTS.DISCONNECT });
});
socket.on(SOCKET_EVENTS.SIOEMIT, (data) => {
process.send({ msgtype: data.name, output: data.message });
});
}
//=============================================================================