Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run Python process as child process of NodeJS. #146

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 40 additions & 6 deletions httpwsd.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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)
Expand Down Expand Up @@ -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();
}

2 changes: 1 addition & 1 deletion mt/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
51 changes: 51 additions & 0 deletions pythonrunner.js
Original file line number Diff line number Diff line change
@@ -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 };
Empty file modified run_AToMPM_local.sh
100755 → 100644
Empty file.