From 638ac52e4053a8b8e5bedeb2726532643274f4ca Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 17 Jun 2016 14:38:08 +0100 Subject: [PATCH 01/19] Move the latexmk timing command into a configurable `latexmkCommandPrefix`. By default, no timing information will be taken. On Linux with GNU user land, this value should be configured to `["/usr/bin/time", "-v"]`. On Mac, gnu-time should be installed and configured to `["/usr/local/bin/gtime", "-v"]`. --- services/clsi/app/coffee/LatexRunner.coffee | 4 +++- services/clsi/config/settings.defaults.coffee | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/LatexRunner.coffee b/services/clsi/app/coffee/LatexRunner.coffee index 65a30464ee..748c277ff4 100644 --- a/services/clsi/app/coffee/LatexRunner.coffee +++ b/services/clsi/app/coffee/LatexRunner.coffee @@ -49,7 +49,9 @@ module.exports = LatexRunner = timings["sys-time"] = stderr?.match(/System time.*: (\d+.\d+)/m)?[1] or 0 callback error, output, stats, timings - _latexmkBaseCommand: ["/usr/bin/time", "-v", "latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] + _latexmkBaseCommand: (Settings?.clsi?.latexmkCommandPrefix || []).concat( + ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] + ) _pdflatexCommand: (mainFile) -> LatexRunner._latexmkBaseCommand.concat [ diff --git a/services/clsi/config/settings.defaults.coffee b/services/clsi/config/settings.defaults.coffee index ae8e132346..f77df86b1a 100644 --- a/services/clsi/config/settings.defaults.coffee +++ b/services/clsi/config/settings.defaults.coffee @@ -28,6 +28,9 @@ module.exports = # modem: # socketPath: false # user: "tex" + # latexmkCommandPrefix: [] + # # latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux + # # latexmkCommandPrefix: ["/usr/local/bin/gtime", "-v"] # on Mac OSX, installed with `brew install gnu-time` internal: clsi: From 03188d2a57070aa3f85e17c84d2ca137841b47a0 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 8 Jun 2016 16:58:08 +0100 Subject: [PATCH 02/19] only keep a single cached output directory in per-user containers --- services/clsi/app/coffee/OutputCacheManager.coffee | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/services/clsi/app/coffee/OutputCacheManager.coffee b/services/clsi/app/coffee/OutputCacheManager.coffee index 7f11bc89b0..25c24b885a 100644 --- a/services/clsi/app/coffee/OutputCacheManager.coffee +++ b/services/clsi/app/coffee/OutputCacheManager.coffee @@ -45,9 +45,8 @@ module.exports = OutputCacheManager = cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR) # Put the files into a new cache subdirectory cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId) - - # let file expiry run in the background - OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId} + # Is it a per-user compile? check if compile directory is PROJECTID-USERID + perUser = Path.basename(compileDir).match(/^[0-9a-f]{24}-[0-9a-f]{24}$/) # Archive logs in background if Settings.clsi?.archive_logs or Settings.clsi?.strace @@ -83,9 +82,15 @@ module.exports = OutputCacheManager = if err? # pass back the original files if we encountered *any* error callback(err, outputFiles) + # clean up the directory we just created + fse.remove cacheDir, (err) -> + if err? + logger.error err: err, dir: dir, "error removing cache dir after failure" else # pass back the list of new files in the cache callback(err, results) + # let file expiry run in the background, expire all previous files if per-user + OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId, limit: if perUser then 0 else null} archiveLogs: (outputFiles, compileDir, buildId, callback = (error) ->) -> archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId) @@ -116,6 +121,8 @@ module.exports = OutputCacheManager = isExpired = (dir, index) -> return false if options?.keep == dir + # remove any directories over the requested (non-null) limit + return true if options?.limit? and index > options.limit # remove any directories over the hard limit return true if index > OutputCacheManager.CACHE_LIMIT # we can get the build time from the first part of the directory name DDDD-RRRR From dd4e1e17b588ab5bcb304d79ff8a40f560b0b0df Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 29 Jun 2016 16:31:08 +0100 Subject: [PATCH 03/19] keep one extra build until per-page pdf serving is enabled --- services/clsi/app/coffee/OutputCacheManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/OutputCacheManager.coffee b/services/clsi/app/coffee/OutputCacheManager.coffee index 25c24b885a..76692b3b71 100644 --- a/services/clsi/app/coffee/OutputCacheManager.coffee +++ b/services/clsi/app/coffee/OutputCacheManager.coffee @@ -90,7 +90,7 @@ module.exports = OutputCacheManager = # pass back the list of new files in the cache callback(err, results) # let file expiry run in the background, expire all previous files if per-user - OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId, limit: if perUser then 0 else null} + OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId, limit: if perUser then 1 else null} archiveLogs: (outputFiles, compileDir, buildId, callback = (error) ->) -> archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId) From 92461742d0cc305bbc4513495067261d50a53bbe Mon Sep 17 00:00:00 2001 From: WaeCo Date: Wed, 13 Jul 2016 13:26:32 -0700 Subject: [PATCH 04/19] Set default project_cache_length_ms to 1 day `project_cache_length_ms` was only `60*60*24 = 1.5 min` which is a little bit short. Default of one day seams more reasonable. --- services/clsi/config/settings.defaults.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/clsi/config/settings.defaults.coffee b/services/clsi/config/settings.defaults.coffee index f77df86b1a..f1f7492a63 100644 --- a/services/clsi/config/settings.defaults.coffee +++ b/services/clsi/config/settings.defaults.coffee @@ -44,5 +44,5 @@ module.exports = url: "http://localhost:3013" smokeTest: false - project_cache_length_ms: 60 * 60 * 24 - parallelFileDownloads:1 \ No newline at end of file + project_cache_length_ms: 1000 * 60 * 60 * 24 + parallelFileDownloads:1 From 9b58621fa8b0dadb124c4236186999c7919ed6ac Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 14 Jul 2016 14:47:36 +0100 Subject: [PATCH 05/19] add support for stopping compile --- services/clsi/app.coffee | 2 ++ services/clsi/app/coffee/CommandRunner.coffee | 24 ++++++++++++++++--- .../clsi/app/coffee/CompileController.coffee | 12 ++++++++-- .../clsi/app/coffee/CompileManager.coffee | 11 +++++++++ services/clsi/app/coffee/LatexRunner.coffee | 15 +++++++++++- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/services/clsi/app.coffee b/services/clsi/app.coffee index fb3224e59d..d13798c4ea 100644 --- a/services/clsi/app.coffee +++ b/services/clsi/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/services/clsi/app/coffee/CommandRunner.coffee b/services/clsi/app/coffee/CommandRunner.coffee index 5ea6765c08..1d0b9e418b 100644 --- a/services/clsi/app/coffee/CommandRunner.coffee +++ b/services/clsi/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/services/clsi/app/coffee/CompileController.coffee b/services/clsi/app/coffee/CompileController.coffee index d4dddd4d82..610baac22e 100644 --- a/services/clsi/app/coffee/CompileController.coffee +++ b/services/clsi/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/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index bb93dbdb9b..87e8734549 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/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/services/clsi/app/coffee/LatexRunner.coffee b/services/clsi/app/coffee/LatexRunner.coffee index 748c277ff4..8619e9baca 100644 --- a/services/clsi/app/coffee/LatexRunner.coffee +++ b/services/clsi/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"] ) From bd1e1de504ce06572f4069eaa54a245849a8f2cf Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 18 Jul 2016 11:05:45 +0100 Subject: [PATCH 06/19] remove dead code --- services/clsi/app/coffee/CompileController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/CompileController.coffee b/services/clsi/app/coffee/CompileController.coffee index 610baac22e..38d53f934b 100644 --- a/services/clsi/app/coffee/CompileController.coffee +++ b/services/clsi/app/coffee/CompileController.coffee @@ -47,7 +47,7 @@ module.exports = CompileController = } stopCompile: (req, res, next) -> - {project_id, user_id, session_id} = req.params + {project_id, user_id} = req.params CompileManager.stopCompile project_id, user_id, (error) -> return next(error) if error? res.sendStatus(204) From 3135ff2842643cf631cfc1b27fc8304e9641baa7 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 26 Jul 2016 12:30:29 +0100 Subject: [PATCH 07/19] add support for passing additional environment parameters to command runner includes an example of passing environment variables to chktex --- services/clsi/app/coffee/CommandRunner.coffee | 9 +++++++-- services/clsi/app/coffee/CompileManager.coffee | 10 +++++++++- services/clsi/app/coffee/LatexRunner.coffee | 6 +++--- .../clsi/test/unit/coffee/CompileManagerTests.coffee | 2 ++ services/clsi/test/unit/coffee/LatexRunnerTests.coffee | 6 ++++-- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/services/clsi/app/coffee/CommandRunner.coffee b/services/clsi/app/coffee/CommandRunner.coffee index 1d0b9e418b..298a495051 100644 --- a/services/clsi/app/coffee/CommandRunner.coffee +++ b/services/clsi/app/coffee/CommandRunner.coffee @@ -4,13 +4,18 @@ logger = require "logger-sharelatex" logger.info "using standard command runner" module.exports = CommandRunner = - run: (project_id, command, directory, image, timeout, callback = (error) ->) -> + run: (project_id, command, directory, image, timeout, environment, callback = (error) ->) -> command = (arg.replace('$COMPILE_DIR', directory) for arg in command) logger.log project_id: project_id, command: command, directory: directory, "running command" logger.warn "timeouts and sandboxing are not enabled with CommandRunner" + # merge environment settings + env = {} + env[key] = value for key, value of process.env + env[key] = value for key, value of environment + # 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 = spawn command[0], command.slice(1), stdio: "inherit", cwd: directory, detached: true, env: env proc.on "error", (err)-> logger.err err:err, project_id:project_id, command: command, directory: directory, "error running command" diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 87e8734549..81c2c03975 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -41,7 +41,14 @@ module.exports = CompileManager = DraftModeManager.injectDraftMode Path.join(compileDir, request.rootResourcePath), callback else callback() - + + # set up environment variables for chktex + env = {} + if request.chktex? + env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -e15 -e16 -e27' + if request.chktex is 'error' + env['CHKTEX_EXIT_ON_ERROR'] = 1 + injectDraftModeIfRequired (error) -> return callback(error) if error? timer = new Metrics.Timer("run-compile") @@ -57,6 +64,7 @@ module.exports = CompileManager = compiler: request.compiler timeout: request.timeout image: request.imageName + environment: env }, (error, output, stats, timings) -> # compile was killed by user if error?.terminated diff --git a/services/clsi/app/coffee/LatexRunner.coffee b/services/clsi/app/coffee/LatexRunner.coffee index 8619e9baca..e743cf0174 100644 --- a/services/clsi/app/coffee/LatexRunner.coffee +++ b/services/clsi/app/coffee/LatexRunner.coffee @@ -8,11 +8,11 @@ ProcessTable = {} # table of currently running jobs (pids or docker container n module.exports = LatexRunner = runLatex: (project_id, options, callback = (error) ->) -> - {directory, mainFile, compiler, timeout, image} = options + {directory, mainFile, compiler, timeout, image, environment} = options compiler ||= "pdflatex" timeout ||= 60000 # milliseconds - logger.log directory: directory, compiler: compiler, timeout: timeout, mainFile: mainFile, "starting compile" + logger.log directory: directory, compiler: compiler, timeout: timeout, mainFile: mainFile, environment: environment, "starting compile" # We want to run latexmk on the tex file which we will automatically # generate from the Rtex/Rmd/md file. @@ -34,7 +34,7 @@ module.exports = LatexRunner = id = "#{project_id}" # record running project under this id - ProcessTable[id] = CommandRunner.run project_id, command, directory, image, timeout, (error, output) -> + ProcessTable[id] = CommandRunner.run project_id, command, directory, image, timeout, environment, (error, output) -> delete ProcessTable[id] return callback(error) if error? runs = output?.stderr?.match(/^Run number \d+ of .*latex/mg)?.length or 0 diff --git a/services/clsi/test/unit/coffee/CompileManagerTests.coffee b/services/clsi/test/unit/coffee/CompileManagerTests.coffee index 611ed11b01..62132f89e4 100644 --- a/services/clsi/test/unit/coffee/CompileManagerTests.coffee +++ b/services/clsi/test/unit/coffee/CompileManagerTests.coffee @@ -47,6 +47,7 @@ describe "CompileManager", -> compiler: @compiler = "pdflatex" timeout: @timeout = 42000 imageName: @image = "example.com/image" + @env = {} @Settings.compileDir = "compiles" @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArg(3) @@ -72,6 +73,7 @@ describe "CompileManager", -> compiler: @compiler timeout: @timeout image: @image + environment: @env }) .should.equal true diff --git a/services/clsi/test/unit/coffee/LatexRunnerTests.coffee b/services/clsi/test/unit/coffee/LatexRunnerTests.coffee index ace3d18ab7..c26fa642b2 100644 --- a/services/clsi/test/unit/coffee/LatexRunnerTests.coffee +++ b/services/clsi/test/unit/coffee/LatexRunnerTests.coffee @@ -22,10 +22,11 @@ describe "LatexRunner", -> @image = "example.com/image" @callback = sinon.stub() @project_id = "project-id-123" + @env = {'foo': '123'} describe "runLatex", -> beforeEach -> - @CommandRunner.run = sinon.stub().callsArg(5) + @CommandRunner.run = sinon.stub().callsArg(6) describe "normally", -> beforeEach -> @@ -35,11 +36,12 @@ describe "LatexRunner", -> compiler: @compiler timeout: @timeout = 42000 image: @image + environment: @env @callback it "should run the latex command", -> @CommandRunner.run - .calledWith(@project_id, sinon.match.any, @directory, @image, @timeout) + .calledWith(@project_id, sinon.match.any, @directory, @image, @timeout, @env) .should.equal true describe "with an .Rtex main file", -> From d6d6e18b895b88285959b0fc723fe383d10307b2 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 26 Jul 2016 16:22:38 +0100 Subject: [PATCH 08/19] run chktex when request has check:true --- services/clsi/app/coffee/CommandRunner.coffee | 4 ++++ services/clsi/app/coffee/CompileController.coffee | 2 ++ services/clsi/app/coffee/CompileManager.coffee | 8 ++++---- services/clsi/app/coffee/RequestParser.coffee | 3 +++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/services/clsi/app/coffee/CommandRunner.coffee b/services/clsi/app/coffee/CommandRunner.coffee index 298a495051..11bbff82d3 100644 --- a/services/clsi/app/coffee/CommandRunner.coffee +++ b/services/clsi/app/coffee/CommandRunner.coffee @@ -27,6 +27,10 @@ module.exports = CommandRunner = err = new Error("terminated") err.terminated = true return callback(err) + else if code + err = new Error("exit") + err.code = code + return callback(err) else callback() diff --git a/services/clsi/app/coffee/CompileController.coffee b/services/clsi/app/coffee/CompileController.coffee index 38d53f934b..3c9af677ae 100644 --- a/services/clsi/app/coffee/CompileController.coffee +++ b/services/clsi/app/coffee/CompileController.coffee @@ -17,6 +17,8 @@ module.exports = CompileController = CompileManager.doCompile request, (error, outputFiles = []) -> if error?.terminated status = "terminated" + else if error?.code is 1 + status = "exited" else if error? logger.error err: error, project_id: request.project_id, "error running compile" if error.timedout diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 81c2c03975..3b19cecaba 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -44,9 +44,9 @@ module.exports = CompileManager = # set up environment variables for chktex env = {} - if request.chktex? - env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -e15 -e16 -e27' - if request.chktex is 'error' + if request.check? + env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16 -w27' + if request.check is 'error' env['CHKTEX_EXIT_ON_ERROR'] = 1 injectDraftModeIfRequired (error) -> @@ -67,7 +67,7 @@ module.exports = CompileManager = environment: env }, (error, output, stats, timings) -> # compile was killed by user - if error?.terminated + if error?.terminated or error?.code is 1 OutputFileFinder.findOutputFiles request.resources, compileDir, (err, outputFiles) -> return callback(err) if err? callback(error, outputFiles) # return output files so user can check logs diff --git a/services/clsi/app/coffee/RequestParser.coffee b/services/clsi/app/coffee/RequestParser.coffee index bd081fd011..5979c75c20 100644 --- a/services/clsi/app/coffee/RequestParser.coffee +++ b/services/clsi/app/coffee/RequestParser.coffee @@ -28,6 +28,9 @@ module.exports = RequestParser = compile.options.draft, default: false, type: "boolean" + response.check = @_parseAttribute "check", + compile.options.check, + type: "string" if response.timeout > RequestParser.MAX_TIMEOUT response.timeout = RequestParser.MAX_TIMEOUT From ffea361d12a3af97cd830ac35bf7fa5a1e415ac5 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 27 Jul 2016 16:54:27 +0100 Subject: [PATCH 09/19] provide validation mode where compilation always exits after chktex --- services/clsi/app/coffee/CompileController.coffee | 4 ++-- services/clsi/app/coffee/CompileManager.coffee | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/services/clsi/app/coffee/CompileController.coffee b/services/clsi/app/coffee/CompileController.coffee index 3c9af677ae..56237bf205 100644 --- a/services/clsi/app/coffee/CompileController.coffee +++ b/services/clsi/app/coffee/CompileController.coffee @@ -17,8 +17,8 @@ module.exports = CompileController = CompileManager.doCompile request, (error, outputFiles = []) -> if error?.terminated status = "terminated" - else if error?.code is 1 - status = "exited" + else if error?.validate + status = "validation-#{error.validate}" else if error? logger.error err: error, project_id: request.project_id, "error running compile" if error.timedout diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 3b19cecaba..4c89a0ba3d 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -48,6 +48,8 @@ module.exports = CompileManager = env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16 -w27' if request.check is 'error' env['CHKTEX_EXIT_ON_ERROR'] = 1 + if request.check is 'validate' + env['CHKTEX_VALIDATE'] = 1 injectDraftModeIfRequired (error) -> return callback(error) if error? @@ -66,8 +68,12 @@ module.exports = CompileManager = image: request.imageName environment: env }, (error, output, stats, timings) -> + if request.check is "validate" + result = if error?.code then "fail" else "pass" + error = new Error("validation") + error.validate = result # compile was killed by user - if error?.terminated or error?.code is 1 + if error?.terminated or error?.validate OutputFileFinder.findOutputFiles request.resources, compileDir, (err, outputFiles) -> return callback(err) if err? callback(error, outputFiles) # return output files so user can check logs From f0c7b455a59605183c9bd08b8027851be5208111 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 29 Jul 2016 14:54:24 +0100 Subject: [PATCH 10/19] provide setting to override child_process.execFile for synctex --- services/clsi/app/coffee/CompileManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 87e8734549..16c36471e3 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -174,7 +174,8 @@ module.exports = CompileManager = _runSynctex: (args, callback = (error, stdout) ->) -> bin_path = Path.resolve(__dirname + "/../../bin/synctex") seconds = 1000 - child_process.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) -> + synctex = Settings.clsi?.synctex?.command?(__dirname, child_process) || child_process + synctex.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) -> if error? logger.err err:error, args:args, "error running synctex" return callback(error) From 08a446f03d974d5e794ffbc3e8d9058b09579220 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 4 Aug 2016 16:07:36 +0100 Subject: [PATCH 11/19] change logging message to be different from LatexRunner --- services/clsi/app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index bf71477c0f..5d14bdf01d 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -28,7 +28,7 @@ module.exports = CompileManager = compileDir = getCompileDir(request.project_id, request.user_id) timer = new Metrics.Timer("write-to-disk") - logger.log project_id: request.project_id, user_id: request.user_id, "starting compile" + logger.log project_id: request.project_id, user_id: request.user_id, "syncing resources to disk" ResourceWriter.syncResourcesToDisk request.project_id, request.resources, compileDir, (error) -> if error? logger.err err:error, project_id: request.project_id, user_id: request.user_id, "error writing resources to disk" From c5917d4a757d1a74d508e5bd3eae62ec9a2dc44c Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 4 Aug 2016 16:08:14 +0100 Subject: [PATCH 12/19] use a command wrapper for synctex instead of an alternative child_process object --- services/clsi/app/coffee/CompileManager.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 5d14bdf01d..34a3a57b89 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -188,8 +188,9 @@ module.exports = CompileManager = _runSynctex: (args, callback = (error, stdout) ->) -> bin_path = Path.resolve(__dirname + "/../../bin/synctex") seconds = 1000 - synctex = Settings.clsi?.synctex?.command?(__dirname, child_process) || child_process - synctex.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) -> + if Settings.clsi?.synctexCommandWrapper? + [bin_path, args] = Settings.clsi?.synctexCommandWrapper bin_path, args + child_process.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) -> if error? logger.err err:error, args:args, "error running synctex" return callback(error) From 872dbc5215a3c2043e96967dbb6d4b623316bef2 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 11 Aug 2016 09:29:03 +0100 Subject: [PATCH 13/19] add missing argument parameter to wordcount call --- services/clsi/app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 34a3a57b89..39cff29d40 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -231,7 +231,7 @@ module.exports = CompileManager = timeout = 10 * 1000 compileName = getCompileName(project_id, user_id) - CommandRunner.run compileName, command, directory, image, timeout, (error) -> + CommandRunner.run compileName, command, directory, image, timeout, {}, (error) -> return callback(error) if error? try stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") From 400590b3ad353b50b846b849ef54ee9b8942b469 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 11 Aug 2016 09:32:47 +0100 Subject: [PATCH 14/19] read wordcount output asynchronously --- services/clsi/app/coffee/CompileManager.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 39cff29d40..e3a218be6f 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -233,12 +233,11 @@ module.exports = CompileManager = CommandRunner.run compileName, command, directory, image, timeout, {}, (error) -> return callback(error) if error? - try - stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") - catch err - logger.err err:err, command:command, directory:directory, project_id:project_id, user_id:user_id, "error reading word count output" - return callback(err) - callback null, CompileManager._parseWordcountFromOutput(stdout) + fs.readFile directory + "/" + file_name + ".wc", "utf-8", (err, stdout) -> + if err? + logger.err err:err, command:command, directory:directory, project_id:project_id, user_id:user_id, "error reading word count output" + return callback(err) + callback null, CompileManager._parseWordcountFromOutput(stdout) _parseWordcountFromOutput: (output) -> results = { From ed5acc9f93bd28cf711fccc445881196ba1398c6 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 11 Aug 2016 09:40:08 +0100 Subject: [PATCH 15/19] capture texcount error output --- services/clsi/app/coffee/CompileManager.coffee | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index e3a218be6f..5192b7fab7 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -226,7 +226,7 @@ module.exports = CompileManager = wordcount: (project_id, user_id, file_name, image, callback = (error, pdfPositions) ->) -> logger.log project_id:project_id, user_id:user_id, file_name:file_name, image:image, "running wordcount" file_path = "$COMPILE_DIR/" + file_name - command = [ "texcount", '-inc', file_path, "-out=" + file_path + ".wc"] + command = [ "texcount", '-nocol', '-inc', file_path, "-out=" + file_path + ".wc"] directory = getCompileDir(project_id, user_id) timeout = 10 * 1000 compileName = getCompileName(project_id, user_id) @@ -237,7 +237,9 @@ module.exports = CompileManager = if err? logger.err err:err, command:command, directory:directory, project_id:project_id, user_id:user_id, "error reading word count output" return callback(err) - callback null, CompileManager._parseWordcountFromOutput(stdout) + results = CompileManager._parseWordcountFromOutput(stdout) + logger.log project_id:project_id, user_id:user_id, wordcount: results, "word count results" + callback null, results _parseWordcountFromOutput: (output) -> results = { @@ -249,6 +251,8 @@ module.exports = CompileManager = elements: 0 mathInline: 0 mathDisplay: 0 + errors: 0 + messages: "" } for line in output.split("\n") [data, info] = line.split(":") @@ -268,4 +272,8 @@ module.exports = CompileManager = results['mathInline'] = parseInt(info, 10) if data.indexOf("Number of math displayed") > -1 results['mathDisplay'] = parseInt(info, 10) + if data is "(errors" # errors reported as (errors:123) + results['errors'] = parseInt(info, 10) + if line.indexOf("!!! ") > -1 # errors logged as !!! message !!! + results['messages'] += line + "\n" return results From a26971ba2e7c36adabaf21d6a52119af58813602 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 11 Aug 2016 10:31:37 +0100 Subject: [PATCH 16/19] update tests --- .../clsi/test/unit/coffee/CompileManagerTests.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/clsi/test/unit/coffee/CompileManagerTests.coffee b/services/clsi/test/unit/coffee/CompileManagerTests.coffee index 62132f89e4..d2b6a100a8 100644 --- a/services/clsi/test/unit/coffee/CompileManagerTests.coffee +++ b/services/clsi/test/unit/coffee/CompileManagerTests.coffee @@ -202,8 +202,8 @@ describe "CompileManager", -> describe "wordcount", -> beforeEach -> - @CommandRunner.run = sinon.stub().callsArg(5) - @fs.readFileSync = sinon.stub().returns @stdout = "Encoding: ascii\nWords in text: 2" + @CommandRunner.run = sinon.stub().callsArg(6) + @fs.readFile = sinon.stub().callsArgWith(2, null, @stdout = "Encoding: ascii\nWords in text: 2") @callback = sinon.stub() @project_id = "project-id-123" @@ -217,10 +217,10 @@ describe "CompileManager", -> it "should run the texcount command", -> @directory = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @file_path = "$COMPILE_DIR/#{@file_name}" - @command =[ "texcount", "-inc", @file_path, "-out=" + @file_path + ".wc"] + @command =[ "texcount", "-nocol", "-inc", @file_path, "-out=" + @file_path + ".wc"] @CommandRunner.run - .calledWith("#{@project_id}-#{@user_id}", @command, @directory, @image, @timeout) + .calledWith("#{@project_id}-#{@user_id}", @command, @directory, @image, @timeout, {}) .should.equal true it "should call the callback with the parsed output", -> @@ -234,5 +234,7 @@ describe "CompileManager", -> elements: 0 mathInline: 0 mathDisplay: 0 + errors: 0 + messages: "" }) .should.equal true From 1af44b32b33bd1b4269094ecf726fc085e95e864 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 22 Aug 2016 15:11:39 +0100 Subject: [PATCH 17/19] remove chktex error too many false positives from 'unable to execute latex command' --- services/clsi/app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 5192b7fab7..356346d308 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -45,7 +45,7 @@ module.exports = CompileManager = # set up environment variables for chktex env = {} if request.check? - env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16 -w27' + env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16' if request.check is 'error' env['CHKTEX_EXIT_ON_ERROR'] = 1 if request.check is 'validate' From 481e842f3325c16cf16fd29ce51658ae697ba9f5 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 24 Aug 2016 15:45:26 +0100 Subject: [PATCH 18/19] fix commandRunner error to match dockerRunner --- services/clsi/app/coffee/CommandRunner.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/clsi/app/coffee/CommandRunner.coffee b/services/clsi/app/coffee/CommandRunner.coffee index 11bbff82d3..f47af00177 100644 --- a/services/clsi/app/coffee/CommandRunner.coffee +++ b/services/clsi/app/coffee/CommandRunner.coffee @@ -27,8 +27,8 @@ module.exports = CommandRunner = err = new Error("terminated") err.terminated = true return callback(err) - else if code - err = new Error("exit") + else if code is 1 # exit status from chktex + err = new Error("exited") err.code = code return callback(err) else From e23512f0e0d90c5a5f3642c8dbb894420f2e0f21 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 24 Aug 2016 15:46:47 +0100 Subject: [PATCH 19/19] handle failed compile due to validation error --- services/clsi/app/coffee/CompileManager.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/clsi/app/coffee/CompileManager.coffee b/services/clsi/app/coffee/CompileManager.coffee index 356346d308..5d2bebb3bc 100644 --- a/services/clsi/app/coffee/CompileManager.coffee +++ b/services/clsi/app/coffee/CompileManager.coffee @@ -68,11 +68,16 @@ module.exports = CompileManager = image: request.imageName environment: env }, (error, output, stats, timings) -> + # request was for validation only if request.check is "validate" result = if error?.code then "fail" else "pass" error = new Error("validation") error.validate = result - # compile was killed by user + # request was for compile, and failed on validation + if request.check is "error" and error?.message is 'exited' + error = new Error("compilation") + error.validate = "fail" + # compile was killed by user, was a validation, or a compile which failed validation if error?.terminated or error?.validate OutputFileFinder.findOutputFiles request.resources, compileDir, (err, outputFiles) -> return callback(err) if err?