diff --git a/app.coffee b/app.coffee index fb3224e..d13798c 100644 --- a/app.coffee +++ b/app.coffee @@ -56,6 +56,7 @@ app.param 'build_id', (req, res, next, build_id) -> app.post "/project/:project_id/compile", bodyParser.json(limit: "5mb"), CompileController.compile +app.post "/project/:project_id/compile/stop", CompileController.stopCompile app.delete "/project/:project_id", CompileController.clearCache app.get "/project/:project_id/sync/code", CompileController.syncFromCode @@ -65,6 +66,7 @@ app.get "/project/:project_id/status", CompileController.status # Per-user containers app.post "/project/:project_id/user/:user_id/compile", bodyParser.json(limit: "5mb"), CompileController.compile +app.post "/project/:project_id/user/:user_id/compile/stop", CompileController.stopCompile app.delete "/project/:project_id/user/:user_id", CompileController.clearCache app.get "/project/:project_id/user/:user_id/sync/code", CompileController.syncFromCode diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 5ea6765..1d0b9e4 100644 --- a/app/coffee/CommandRunner.coffee +++ b/app/coffee/CommandRunner.coffee @@ -9,9 +9,27 @@ module.exports = CommandRunner = logger.log project_id: project_id, command: command, directory: directory, "running command" logger.warn "timeouts and sandboxing are not enabled with CommandRunner" - proc = spawn command[0], command.slice(1), stdio: "inherit", cwd: directory + # run command as detached process so it has its own process group (which can be killed if needed) + proc = spawn command[0], command.slice(1), stdio: "inherit", cwd: directory, detached: true + proc.on "error", (err)-> logger.err err:err, project_id:project_id, command: command, directory: directory, "error running command" callback(err) - proc.on "close", () -> - callback() \ No newline at end of file + + proc.on "close", (code, signal) -> + logger.info code:code, signal:signal, project_id:project_id, "command exited" + if signal is 'SIGTERM' # signal from kill method below + err = new Error("terminated") + err.terminated = true + return callback(err) + else + callback() + + return proc.pid # return process id to allow job to be killed if necessary + + kill: (pid, callback = (error) ->) -> + try + process.kill -pid # kill all processes in group + catch err + return callback(err) + callback() diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index d4dddd4..610baac 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -15,7 +15,9 @@ module.exports = CompileController = ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? CompileManager.doCompile request, (error, outputFiles = []) -> - if error? + if error?.terminated + status = "terminated" + else if error? logger.error err: error, project_id: request.project_id, "error running compile" if error.timedout status = "timedout" @@ -43,7 +45,13 @@ module.exports = CompileController = type: file.type build: file.build } - + + stopCompile: (req, res, next) -> + {project_id, user_id, session_id} = req.params + CompileManager.stopCompile project_id, user_id, (error) -> + return next(error) if error? + res.sendStatus(204) + clearCache: (req, res, next = (error) ->) -> ProjectPersistenceManager.clearProject req.params.project_id, req.params.user_id, (error) -> return next(error) if error? diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index bb93dbd..87e8734 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -58,6 +58,13 @@ module.exports = CompileManager = timeout: request.timeout image: request.imageName }, (error, output, stats, timings) -> + # compile was killed by user + if error?.terminated + OutputFileFinder.findOutputFiles request.resources, compileDir, (err, outputFiles) -> + return callback(err) if err? + callback(error, outputFiles) # return output files so user can check logs + return + # compile completed normally return callback(error) if error? Metrics.inc("compiles-succeeded") for metric_key, metric_value of stats or {} @@ -78,6 +85,10 @@ module.exports = CompileManager = OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> callback null, newOutputFiles + stopCompile: (project_id, user_id, callback = (error) ->) -> + compileName = getCompileName(project_id, user_id) + LatexRunner.killLatex compileName, callback + clearProject: (project_id, user_id, _callback = (error) ->) -> callback = (error) -> _callback(error) diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 748c277..8619e9b 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -4,6 +4,8 @@ logger = require "logger-sharelatex" Metrics = require "./Metrics" CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner") +ProcessTable = {} # table of currently running jobs (pids or docker container names) + module.exports = LatexRunner = runLatex: (project_id, options, callback = (error) ->) -> {directory, mainFile, compiler, timeout, image} = options @@ -30,7 +32,10 @@ module.exports = LatexRunner = if Settings.clsi?.strace command = ["strace", "-o", "strace", "-ff"].concat(command) - CommandRunner.run project_id, command, directory, image, timeout, (error, output) -> + id = "#{project_id}" # record running project under this id + + ProcessTable[id] = CommandRunner.run project_id, command, directory, image, timeout, (error, output) -> + delete ProcessTable[id] return callback(error) if error? runs = output?.stderr?.match(/^Run number \d+ of .*latex/mg)?.length or 0 failed = if output?.stdout?.match(/^Latexmk: Errors/m)? then 1 else 0 @@ -49,6 +54,14 @@ module.exports = LatexRunner = timings["sys-time"] = stderr?.match(/System time.*: (\d+.\d+)/m)?[1] or 0 callback error, output, stats, timings + killLatex: (project_id, callback = (error) ->) -> + id = "#{project_id}" + logger.log {id:id}, "killing running compile" + if not ProcessTable[id]? + return callback new Error("no such project to kill") + else + CommandRunner.kill ProcessTable[id], callback + _latexmkBaseCommand: (Settings?.clsi?.latexmkCommandPrefix || []).concat( ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] )