diff --git a/README.md b/README.md index a2f7018..15f1103 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ To run AToMPM on Windows, double-click on the `run.bat` script inside of the mai ### Mac or Linux 1. Execute `node httpwsd.js` in one terminal 2. Execute `python mt\main.py` in another terminal +2.1. Alternatively, adding the `--with-python` flag to the `node` command above will run the Python process as a child of the NodeJS process, making this step unnecessary. 3. Open a browser (Firefox or Chrome) and navigate to [http://localhost:8124/atompm](http://localhost:8124/atompm) * The above steps are automated by the `run_AToMPM_local.sh` script diff --git a/httpwsd.js b/httpwsd.js index 01c7e66..68af6bb 100644 --- a/httpwsd.js +++ b/httpwsd.js @@ -4,7 +4,8 @@ */ /*********************************** IMPORTS **********************************/ -const _cp = require('child_process'), +const process = require('node:process'); +const _cp = require('node:child_process'), _fs = require('fs'), _http = require('http'), _url = require('url'), @@ -14,6 +15,11 @@ const _cp = require('child_process'), _utils = require('./utils'); const session_manager = require("./session_manager"); +// Command line flag: Start python model transformation server process as a child process of this (node) process. +const runWithPythonChildProcess = process.argv.slice(2).includes("--with-python"); // opt-in + +const port = 8124; + /** Wrapper function to log HTTP messages from the server **/ function __respond(response, statusCode, reason, data, headers) @@ -603,10 +609,38 @@ let httpserver = _http.createServer( session_manager.init_session_manager(httpserver); -let port = 8124; -httpserver.listen(port); +function startServer() { + httpserver.listen(port); + logger.info(`Server listening on: http://localhost:${port}/atompm`); + logger.info("```mermaid"); + logger.info("sequenceDiagram"); + + function gracefulShutdown() { + logger.info("Gracefully shutting down..."); + httpserver.close(); + } + process.once('SIGINT', () => { + // The Python process belongs to the same process group, and will also receive a SIGINT. + // This is not necessary, because we send a SIGTERM to the Python process anyway, but it won't cause us trouble. + + // The next SIGINT will cause a forced exit: + process.once('SIGINT', () => { + process.exit(1); + }); + logger.info(""); + logger.info("Received SIGINT. Send another SIGINT to force shutdown."); + gracefulShutdown(); + }); + process.once('SIGTERM', gracefulShutdown); +} -logger.info("AToMPM listening on port: " + port); -logger.info("```mermaid"); -logger.info("sequenceDiagram"); +if (runWithPythonChildProcess) { + const {startPythonChildProcess} = require("./pythonrunner"); + const {endPythonChildProcess} = startPythonChildProcess(startServer); + httpserver.on('close', endPythonChildProcess); +} +else { + // Run without Python as a child process + startServer(); +} diff --git a/mt/main.py b/mt/main.py index 75a3f7b..be547d4 100644 --- a/mt/main.py +++ b/mt/main.py @@ -12,9 +12,9 @@ def main() : logging.basicConfig(format='%(levelname)s - %(message)s', level=logging.INFO) - print("Starting Model Transformation Server... ") httpd = HTTPServerThread() httpd.start() + print("Started Model Transformation Server") if __name__ == "__main__" : main() diff --git a/pythonrunner.js b/pythonrunner.js new file mode 100644 index 0000000..78e2a16 --- /dev/null +++ b/pythonrunner.js @@ -0,0 +1,51 @@ +const childProcess = require("node:child_process"); +const logger = require("./logger"); + +function startPythonChildProcess(pythonStartedCallback) { + const pythonProcess = childProcess.spawn("python", ["mt/main.py"]); + pythonProcess.on("exit", (code, signal) => { + logger.info("Model Transformation Server exited " + + ((code===null) ? ("by signal " + signal) : ("with code " + code.toString()))); + }); + const coloredStream = (readableStream, writableStream, colorCode) => { + readableStream.on("data", chunk => { + writableStream.write("\x1b["+colorCode+"m"); // set color + writableStream.write(chunk); + writableStream.write("\x1b[0m"); // reset color + }); + }; + // output of Python process is interleaved with output of this process: + coloredStream(pythonProcess.stdout, process.stdout, "33"); // yellow + coloredStream(pythonProcess.stderr, process.stderr, "91"); // red + + // Only start the HTTP server after the Transformation Server has started. + // When the Python process has written the following string to stdout, we know the transformation server has started: + const expectedString = Buffer.from("Started Model Transformation Server\n"); + let accumulatedOutput = Buffer.alloc(0); + function pythonOutputListener(chunk) { + accumulatedOutput = Buffer.concat([accumulatedOutput, chunk]); + + if (accumulatedOutput.length >= expectedString.length) { + if (accumulatedOutput.subarray(0, expectedString.length).equals(expectedString)) { + // No need to keep accumulating pythonProcess" stdout: + pythonProcess.stdout.removeListener("data", pythonOutputListener); + + pythonStartedCallback(); + } + } + } + pythonProcess.stdout.on("data", pythonOutputListener); + + // In case of a forced exit (e.g. uncaught exception), the Python process may still be running, so we force-kill it: + process.on("exit", code => { + pythonProcess.kill("SIGKILL"); + }); + + return { + endPythonChildProcess: () => { + pythonProcess.kill("SIGTERM"); // cleanly exit Python process AFTER http server has shut down. + }, + }; +} + +module.exports = { startPythonChildProcess }; \ No newline at end of file diff --git a/run_AToMPM_local.sh b/run_AToMPM_local.sh old mode 100755 new mode 100644