diff --git a/api/package.json b/api/package.json index 0c32198..e8e5b5d 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "piston-api", - "version": "3.0.0", + "version": "3.1.0", "description": "API for piston - a high performance code execution engine", "main": "src/index.js", "dependencies": { diff --git a/api/src/api/v2.js b/api/src/api/v2.js index 487b10b..0c34d70 100644 --- a/api/src/api/v2.js +++ b/api/src/api/v2.js @@ -9,6 +9,8 @@ const { Job } = require('../job'); const package = require('../package'); const logger = require('logplease').create('api/v2'); +const SIGNALS = ["SIGABRT","SIGALRM","SIGBUS","SIGCHLD","SIGCLD","SIGCONT","SIGEMT","SIGFPE","SIGHUP","SIGILL","SIGINFO","SIGINT","SIGIO","SIGIOT","SIGKILL","SIGLOST","SIGPIPE","SIGPOLL","SIGPROF","SIGPWR","SIGQUIT","SIGSEGV","SIGSTKFLT","SIGSTOP","SIGTSTP","SIGSYS","SIGTERM","SIGTRAP","SIGTTIN","SIGTTOU","SIGUNUSED","SIGURG","SIGUSR1","SIGUSR2","SIGVTALRM","SIGXCPU","SIGXFSZ","SIGWINCH"] +// ref: https://man7.org/linux/man-pages/man7/signal.7.html function get_job(body){ const { @@ -148,27 +150,28 @@ router.ws('/connect', async (ws, req) => { try{ const msg = JSON.parse(data); - if(msg.type === "init"){ - if(job === null){ - const job = await get_job(msg); + switch(msg.type){ + case "init": + if(job === null){ + job = await get_job(msg); - await job.prime(); + await job.prime(); - ws.send(JSON.stringify({ - type: "runtime", - language: job.runtime.language, - version: job.runtime.version.raw - })) + ws.send(JSON.stringify({ + type: "runtime", + language: job.runtime.language, + version: job.runtime.version.raw + })) - await job.execute_interactive(eventBus); + await job.execute_interactive(eventBus); - ws.close(4999, "Job Completed"); + ws.close(4999, "Job Completed"); - }else{ - ws.close(4000, "Already Initialized"); - } - - }else if(msg.type === "data"){ + }else{ + ws.close(4000, "Already Initialized"); + } + break; + case "data": if(job !== null){ if(msg.stream === "stdin"){ eventBus.emit("stdin", msg.data) @@ -178,7 +181,20 @@ router.ws('/connect', async (ws, req) => { }else{ ws.close(4003, "Not yet initialized") } + break; + case "signal": + if(job !== null){ + if(SIGNALS.includes(msg.signal)){ + eventBus.emit("signal", msg.signal) + }else{ + ws.close(4005, "Invalid signal") + } + }else{ + ws.close(4003, "Not yet initialized") + } + break; } + }catch(error){ ws.send(JSON.stringify({type: "error", message: error.message})) ws.close(4002, "Notified Error") @@ -194,8 +210,8 @@ router.ws('/connect', async (ws, req) => { setTimeout(()=>{ //Terminate the socket after 1 second, if not initialized. - //if(job === null) - // ws.close(4001, "Initialization Timeout"); + if(job === null) + ws.close(4001, "Initialization Timeout"); }, 1000) }) diff --git a/api/src/job.js b/api/src/job.js index 8001a76..d213bbc 100644 --- a/api/src/job.js +++ b/api/src/job.js @@ -110,7 +110,13 @@ class Job { eventBus.on("stdin", (data) => { proc.stdin.write(data); }) + + eventBus.on("kill", (signal) => { + proc.kill(signal) + }) } + + const kill_timeout = set_timeout( _ => proc.kill('SIGKILL'), @@ -118,20 +124,22 @@ class Job { ); proc.stderr.on('data', data => { - if (stderr.length > config.output_max_size) { + if(eventBus !== null) { + eventBus.emit("stderr", data); + } else if (stderr.length > config.output_max_size) { proc.kill('SIGKILL'); } else { - if(eventBus !== null) eventBus.emit("stderr", data); stderr += data; output += data; } }); proc.stdout.on('data', data => { - if (stdout.length > config.output_max_size) { + if(eventBus !== null){ + eventBus.emit("stdout", data); + } else if (stdout.length > config.output_max_size) { proc.kill('SIGKILL'); } else { - if(eventBus !== null) eventBus.emit("stdout", data); stdout += data; output += data; } diff --git a/cli/commands/execute.js b/cli/commands/execute.js index e273548..abb1f63 100644 --- a/cli/commands/execute.js +++ b/cli/commands/execute.js @@ -1,7 +1,10 @@ -//const fetch = require('node-fetch'); const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); +const WebSocket = require('ws'); + +const SIGNALS = ["SIGABRT","SIGALRM","SIGBUS","SIGCHLD","SIGCLD","SIGCONT","SIGEMT","SIGFPE","SIGHUP","SIGILL","SIGINFO","SIGINT","SIGIO","SIGIOT","SIGLOST","SIGPIPE","SIGPOLL","SIGPROF","SIGPWR","SIGQUIT","SIGSEGV","SIGSTKFLT","SIGTSTP","SIGSYS","SIGTERM","SIGTRAP","SIGTTIN","SIGTTOU","SIGUNUSED","SIGURG","SIGUSR1","SIGUSR2","SIGVTALRM","SIGXCPU","SIGXFSZ","SIGWINCH"] + exports.command = ['execute [args..]']; exports.aliases = ['run']; @@ -35,17 +38,115 @@ exports.builder = { alias: ['f'], array: true, desc: 'Additional files to add', + }, + interactive: { + boolean: true, + alias: ['t'], + desc: 'Run interactively using WebSocket transport' + }, + status: { + boolean: true, + alias: ['s'], + desc: 'Output additional status to stderr' } }; -exports.handler = async (argv) => { - const files = [...(argv.files || []),argv.file] - .map(file_path => { - return { - name: path.basename(file_path), - content: fs.readFileSync(file_path).toString() - }; - }); +async function handle_interactive(files, argv){ + const ws = new WebSocket(argv.pistonUrl.replace("http", "ws") + "/api/v2/connect") + + const log_message = (process.stderr.isTTY && argv.status) ? console.error : ()=>{}; + + process.on("exit", ()=>{ + ws.close(); + process.stdin.end(); + process.stdin.destroy(); + process.exit(); + }) + + for(const signal of SIGNALS){ + process.on(signal, ()=>{ + ws.send(JSON.stringify({type: 'signal', signal})) + }) + } + + + + ws.on('open', ()=>{ + const request = { + type: "init", + language: argv.language, + version: argv['language_version'], + files: files, + args: argv.args, + compile_timeout: argv.ct, + run_timeout: argv.rt + } + + ws.send(JSON.stringify(request)) + log_message(chalk.white.bold("Connected")) + + process.stdin.resume(); + + process.stdin.on("data", (data) => { + ws.send(JSON.stringify({ + type: "data", + stream: "stdin", + data: data.toString() + })) + }) + }) + + ws.on("close", (code, reason)=>{ + log_message( + chalk.white.bold("Disconnected: "), + chalk.white.bold("Reason: "), + chalk.yellow(`"${reason}"`), + chalk.white.bold("Code: "), + chalk.yellow(`"${code}"`), + ) + process.stdin.pause() + }) + + ws.on('message', function(data){ + const msg = JSON.parse(data); + + switch(msg.type){ + case "runtime": + log_message(chalk.bold.white("Runtime:"), chalk.yellow(`${msg.language} ${msg.version}`)) + break; + case "stage": + log_message(chalk.bold.white("Stage:"), chalk.yellow(msg.stage)) + break; + case "data": + if(msg.stream == "stdout") process.stdout.write(msg.data) + else if(msg.stream == "stderr") process.stderr.write(msg.data) + else log_message(chalk.bold.red(`(${msg.stream}) `), msg.data) + break; + case "exit": + if(msg.signal === null) + log_message( + chalk.white.bold("Stage"), + chalk.yellow(msg.stage), + chalk.white.bold("exited with code"), + chalk.yellow(msg.code) + ) + else + log_message( + chalk.white.bold("Stage"), + chalk.yellow(msg.stage), + chalk.white.bold("exited with signal"), + chalk.yellow(msg.signal) + ) + break; + default: + log_message(chalk.red.bold("Unknown message:"), msg) + } + }) + +} + +async function run_non_interactively(files, argv) { + const stdin = (argv.stdin && await new Promise((resolve, _) => { let data = ''; @@ -99,3 +200,18 @@ exports.handler = async (argv) => { step('Run', response.run); } + +exports.handler = async (argv) => { + const files = [...(argv.files || []),argv.file] + .map(file_path => { + return { + name: path.basename(file_path), + content: fs.readFileSync(file_path).toString() + }; + }); + + if(argv.interactive) await handle_interactive(files, argv); + else await run_non_interactively(files, argv); +} + + diff --git a/cli/package-lock.json b/cli/package-lock.json index d564e5f..76a2cc6 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -14,6 +14,7 @@ "minimatch": "^3.0.4", "nocamel": "^1.0.2", "semver": "^7.3.5", + "ws": "^7.5.3", "yargs": "^16.2.0" } }, @@ -243,6 +244,26 @@ "node": ">=10" } }, + "node_modules/ws": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", @@ -455,6 +476,12 @@ "strip-ansi": "^6.0.0" } }, + "ws": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "requires": {} + }, "y18n": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", diff --git a/cli/package.json b/cli/package.json index 6df989d..90e3e12 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "piston-cli", - "version": "1.0.0", + "version": "1.1.0", "description": "Piston Execution Engine CLI tools", "main": "index.js", "license": "MIT", @@ -10,6 +10,7 @@ "minimatch": "^3.0.4", "nocamel": "^1.0.2", "semver": "^7.3.5", + "ws": "^7.5.3", "yargs": "^16.2.0" } } diff --git a/piston b/piston index 2dc36fa..7e3a469 100755 --- a/piston +++ b/piston @@ -35,11 +35,12 @@ case $1 in echo " clean-pkgs Clean any package build artifacts on disk" echo " clean-repo Remove all packages from local repo" echo " build-pkg Build a package" + echo " rebuild Build and restart the docker container" else echo " Switch to developement environment for more info" - echo " > piston switch dev" + echo " > piston select dev" fi ;; @@ -52,6 +53,8 @@ case $1 in start) docker_compose up -d ;; stop) docker_compose down ;; + rebuild) docker_compose build && docker_compose up -d ;; + update) git pull docker_compose pull